<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>J10c&apos;s Blog</title><description>自己写的博客</description><link>https://site.j10ccc.xyz/</link><follow_challenge><feedId>62033118826866689</feedId><userId>62125606033830912</userId></follow_challenge><item><title>二月的几个趣事2022</title><link>https://site.j10ccc.xyz/zh-cn/blog/anecdotes-feb-2022-zh-cn/</link><guid isPermaLink="true">https://site.j10ccc.xyz/zh-cn/blog/anecdotes-feb-2022-zh-cn/</guid><pubDate>Thu, 17 Feb 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;静电对笔记本蓝牙的影响&lt;/h2&gt;
&lt;p&gt;手上这台 &lt;strong&gt;小米PRO&lt;/strong&gt; 笔记本买来有4年了，近两年一直被一个问题困扰，就是电脑用着用着蓝牙断掉，确认了不是外设的问题后重新打开电脑蓝牙，蓝牙开关消失，去设备管理器一看，有一个硬件识别失败，报错&lt;code&gt;设备请求符描述失败&lt;/code&gt;。 这样出现好几次，找过好多教程都没有用，什么把服务自动改成手动，一个都没有用，最后都是重启电脑若干次，随缘解决问题。蓝牙回来了之后就没管他了。&lt;/p&gt;
&lt;p&gt;今年这次急用电脑，又给我出幺蛾子，备份了一下资料重装系统了发现蓝牙还是识别不了，于是又去网上查，这次换了种问法查（笔记本蓝牙图标消失） ，没想到这次终于看到了不一样的答案。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.zhihu.com/question/49943281/answer/1067858170&quot;&gt;求助！笔记本蓝牙突然消失不见了啊！？ - 知乎 (zhihu.com)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;讲的很明白，是插电使用时，&lt;strong&gt;静电&lt;/strong&gt;导致蓝牙硬件故障，这个估计是我这款电脑的设计缺陷。物理释放静电和长按开机键主动释放静电都有效，&lt;s&gt;这次物理放电手法不对还是咋滴，效果看不出来&lt;/s&gt;，倒是是按开机键有用，长按开机键几十秒会有系统自带的修复程序启动，再重启一下就好了。&lt;/p&gt;
&lt;h2&gt;HUE&lt;/h2&gt;
&lt;p&gt;过年写烟花前端实现的时候，在配色相关的参数代码中出现了&lt;strong&gt;hue&lt;/strong&gt;这个词。去wiki里面查了一下：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;HSV是一种将RGB色彩模型中的点在圆柱坐标系中的表示法。&lt;/p&gt;
&lt;p&gt;HSL即色相、饱和度、亮度（英语：Hue, Saturation, Lightness）&lt;/p&gt;
&lt;p&gt;HSV即色相、饱和度、明度（英语：Hue, Saturation, Value）&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://zh.wikipedia.org/wiki/HSL%E5%92%8CHSV%E8%89%B2%E5%BD%A9%E7%A9%BA%E9%97%B4&quot;&gt;HSL和HSV色彩空间 - 维基百科，自由的百科全书 (wikipedia.org)&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;目前理解：hue就是一个角度数值，倒圆锥底部一圈从0开始到360，具体颜色和角度的关系看代码：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/j10ccc/fireworks&quot;&gt;j10ccc/fireworks: A fireworks demo developed in javascript (github.com)&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const color = 120;
/*
	red: 0,
	yellow: 60,
	green: 120,
	cyan: 180,
	blue: 240,
	magenta: 300
*/
var hue = Math.floor(Math.random() * 51) + color;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Rime输入法&lt;/h2&gt;
&lt;p&gt;因为win11的渣调度方案，我的电脑不插电时使用微软拼音输入法，巨卡无比。想起之前总监转发过一个跨平台的输入法，想着用第三方输入法应该解决问题。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://rime.im/&quot;&gt;RIME | 中州韻輸入法引擎&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这个输入法搭配四叶草拼音引擎输入比较顺手，&lt;s&gt;不过肯定没有微软电脑自带的有云词库这些智能的东西&lt;/s&gt;。就跟安卓上的 &lt;strong&gt;Gboard&lt;/strong&gt; 一样，刚开始用比较生涩，词库得靠自己积累。&lt;/p&gt;
&lt;p&gt;电脑大键盘打字，基本上都是全拼输入，所以词库拉一点没什么问题，输入中文延迟的问题终于解决了（这个感觉是电脑低压U的问题&lt;/p&gt;
</content:encoded></item><item><title>蚂蚁技术体验部大作业总结</title><link>https://site.j10ccc.xyz/zh-cn/blog/ant-chatroom-zh-cn/</link><guid isPermaLink="true">https://site.j10ccc.xyz/zh-cn/blog/ant-chatroom-zh-cn/</guid><pubDate>Sun, 16 Jan 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;背景&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;这次前端课收获颇丰，蚂蚁高层讲师水平高，代码风格棒，教学态度也很好，课堂氛围融洽~~（第一节课的老师甚至跟我聊起了 HHKB）~~&lt;/li&gt;
&lt;li&gt;期末还差两门考试在一个星期后，突然想起来博客好像好久没有更新了。&lt;/li&gt;
&lt;li&gt;今天大家好像也都不想复习了，就来写这份总结&lt;/li&gt;
&lt;li&gt;博客的代码框不知道为啥变回黄色了，可能是 Linux 下 hugo 的锅，反正懒得再改了（&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;技术栈&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;NodeJS&lt;/li&gt;
&lt;li&gt;koa&lt;/li&gt;
&lt;li&gt;socket.io&lt;/li&gt;
&lt;li&gt;客户端&lt;/li&gt;
&lt;li&gt;服务端&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;koa&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://koa.bootcss.com/&quot;&gt;Koa&lt;/a&gt;是一个基于 Node.js 平台的下一代 web 开发框架。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;s&gt;上半节课用原生 node 写的 http 服务器代码忘记放哪里去了，反正 koa 10行代码就能搞定&lt;/s&gt;&lt;/p&gt;
&lt;h3&gt;注意&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;koa&lt;/code&gt;代码全写在&lt;strong&gt;后端程序&lt;/strong&gt;里面&lt;/p&gt;
&lt;h3&gt;引入&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const path = require(&apos;path&apos;);
const http = require(&apos;http&apos;);
const Koa = require(&apos;koa&apos;);
const serve = require(&apos;koa-static&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;模板&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// 初始化
const hostname = &apos;127.0.0.1&apos;;
const port = 3000;
const publicPath = path.join(__dirname, &apos;public&apos;);

// 创建koa实例
const app = new Koa();

// 创建http server 实例
const server = http.createServer(app.callback());
/*
   code here...
 */

app.use(serve(publicPath));

server.listen(port, hostname, () =&amp;gt; {
	console.log(&apos;listening...&apos;);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;socket.io&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://socketio.bootcss.com/&quot;&gt;Socket.IO&lt;/a&gt; 是一个可以在浏览器与服务器之间实现实时、双向、基于事件的通信的工具库。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;s&gt;写这部分代码太痛苦了，写错了一个对象一直没发现，还以为是 wsl 的问题&lt;/s&gt;&lt;/p&gt;
&lt;h3&gt;注意&lt;/h3&gt;
&lt;p&gt;socket.io在客户端最好不要使用本地引用，好像会出锅？控制台一直会报错，不知道为啥&lt;/p&gt;
&lt;h3&gt;服务端&lt;/h3&gt;
&lt;h5&gt;主框架&lt;/h5&gt;
&lt;p&gt;以下代码写在上面的&lt;strong&gt;koa代码&lt;/strong&gt;的&lt;code&gt;code here&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;/*
   以下代码是写在上面的koa代码的 code here 处的
 */

//创建 socket.io实例
const io = socketIO(server);

//储存在线所有用户
const users = new Map();
const historys = [];

// 客户端接入
io.use((socket, next) =&amp;gt; {
	const { name, password } = socket.handshake.query;
	if (!name) {
		return next(new Error(&apos;WRONG_ACCOUNT&apos;));
	}
	if (password !== &apos;j10c&apos;) {
		console.log(&amp;quot;wrong password&amp;quot;);
		return next(new Error(&apos;WRONG_PASSWORD&apos;));
	}
	next();
});

io.on(&apos;connection&apos;, function(socket) {
	console.log(&apos;user connected&apos;);
	const name = socket.handshake.query.name;
	users.set(name, socket); // 在表中储存用户消息
	console.log(&apos;users====&apos;, users.keys());
	io.sockets.emit(&apos;online&apos;, [...users.keys()]); // 注意 sockets ！！！

	socket.on(&apos;disconnect&apos;, function(socket) {
		console.log(socket); //transport close
		console.log(&apos;user connected&apos;);
		users.delete(name, socket);
		console.log(&apos;users ==== &apos;, users.keys());
		io.sockets.emit(&apos;online&apos;, [...users.keys()]); // 服务端向所有客户端发送数据 io.sockets.emit
	});

	socket.on(&apos;sendMessage&apos;, (content) =&amp;gt; {
		console.log(name + &amp;quot; send a message: &amp;quot; + content);
		const message = {
			time: Date.now(),
			sender: name,
			content: content
		};
		historys.push(message);
		socket.broadcast.emit(&apos;receiveMessage&apos;, message);// 服务端向所有其他客户端发送数据
	});

	socket.on(&apos;getHistory&apos;, (fn) =&amp;gt; {
		fn(historys);
	});
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;&lt;code&gt;io&lt;/code&gt;实例&lt;/h4&gt;
&lt;p&gt;主要代码分成两部分&lt;code&gt;io.use&lt;/code&gt; 和&lt;code&gt;io.on&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;服务端的&lt;code&gt;socket&lt;/code&gt;是&lt;code&gt;io.use&lt;/code&gt;或者&lt;code&gt;io.on&lt;/code&gt;中调回函数中的一个参数，所以&lt;code&gt;socket.on&lt;/code&gt;是写在&lt;code&gt;io.on(){}&lt;/code&gt;函数里面的，注意！！！&lt;/p&gt;
&lt;h4&gt;发送数据&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;io.sockets.emit(eventName, data)&lt;/code&gt;，&lt;code&gt;io.sockets.send(data)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;如代码中的&lt;code&gt;io.sockets.emit(&apos;online&apos;, [...users.keys()]);&lt;/code&gt; ，即向所有已经建立连接的&lt;strong&gt;客户端&lt;/strong&gt;发送数据&lt;/p&gt;
&lt;h4&gt;广播&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;socket.broadcast.emit&lt;/code&gt;或者&lt;code&gt;socket.broadcast.emit&lt;/code&gt;，向所有已建立连接的&lt;strong&gt;其他客户端&lt;/strong&gt;发送数据**（注意和&lt;code&gt;io.sockets.emit&lt;/code&gt;的区分！！！）**&lt;/p&gt;
&lt;h4&gt;区分&lt;/h4&gt;
&lt;p&gt;在这个项目中，用户向房间发送消息之后，他自己的消息同时发到本地缓存和服务器，服务器无需再给该用户发送消息数据！所以用&lt;code&gt;socket.broadcast.emit&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;客户端&lt;/h3&gt;
&lt;h4&gt;程序流程&lt;/h4&gt;
&lt;p&gt;登录，检查登录成功与否，获取历史消息&lt;/p&gt;
&lt;h4&gt;主框架&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;var socket; //开头先声明 socket 为全局变量
window.onload = function() {
	document.querySelector(&apos;#submit&apos;).addEventListener(&apos;click&apos;, ()=&amp;gt;{
		socket = io({
			query: {
			name: account,
			password: password
		},
		reconnection: false,
	});
	socket.on(&apos;connect&apos;, () =&amp;gt; {
		socket.emit(&apos;getHistory&apos;, (data) =&amp;gt; {});
	});
	socket.on(&apos;connect_error&apos;, (err) =&amp;gt; {});
	socket.on(&apos;disconnect&apos;, () =&amp;gt; {});
	socket.on(&apos;online&apos;, (users) =&amp;gt; {}); // 监听服务端发来的数据
	socket.on(&apos;receiveMessage&apos;, (message) =&amp;gt; {});
}
                                                       
document.querySelector(&apos;#sent_message&apos;).addEventListener(&apos;click&apos;, (e) =&amp;gt; {
	socket.emit(&apos;sendMessage&apos;, text);
})；

&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;解析&lt;/h4&gt;
&lt;p&gt;在第一次用到&lt;code&gt;socket&lt;/code&gt;的时候给变量赋值，下面的代码是在**登录(login)**的时候就用到了&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意区分服务端和客户端中的 &lt;code&gt;socket&lt;/code&gt;！！！&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;login.addEventListener(&apos;click&apos;, function(){
	var account = document.querySelectorAll(&apos;input&apos;)[0].value;
	var password = document.querySelectorAll(&apos;input&apos;)[1].value;
	socket = io({
		query: {
			name: account,
			password: password
		},
		reconnection: false,
	});
});
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;客户端的&lt;code&gt;socket.emit(eventName, data, [callback])&lt;/code&gt;作用为给服务端发送数据，服务端通过&lt;code&gt;socket.on(eventName, (data, fn) =&amp;gt; {})&lt;/code&gt;创建监听数据事件，两者间通过&lt;code&gt;eventName&lt;/code&gt;相互匹配。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[callback]&lt;/code&gt;为回调函数，用于指定一个当对方确定接收到数据时调用的回调函数。&lt;/li&gt;
&lt;li&gt;另外一种监听事件&lt;code&gt;socket.once(eventName, (data, fn) =&amp;gt; {})&lt;/code&gt;，这个只监听一次，在回调函数执行完毕后，结束监听。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;一个很有意思的东西&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;//客户端
socket.emit(&apos;sendMessage&apos;, text);
socket.emit(&apos;getHistory&apos;, (data) =&amp;gt; {});

//服务端
socket.on(&apos;sendMessage&apos;, (content) =&amp;gt; {
	console.log(name + &amp;quot; send a message: &amp;quot; + content);
});
socket.on(&apos;getHistory&apos;, (fn) =&amp;gt; {
	fn(historys);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;sendMessage&lt;/code&gt;中客户端发送了&lt;code&gt;text&lt;/code&gt;，服务端中用回调函数中的&lt;code&gt;content&lt;/code&gt;参数接收&lt;/p&gt;
&lt;p&gt;而&lt;code&gt;getHsitory&lt;/code&gt;中客户端发送了空内容，来请求数据，服务端用另外一个参数&lt;code&gt;fn&lt;/code&gt;（应该是个函数），这个函数可以立即将数据发送回去，这个是不是有点类似 get 请求？&lt;/p&gt;
&lt;h2&gt;前端代码的一些tips&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;HTML 结构语义化&lt;/li&gt;
&lt;li&gt;缩进长度两个空格？蚂蚁这位学长的代码风格非常满足我强迫症，&lt;s&gt;然而这篇文章是用 typora写的，代码都是乱贴的，缩进很乱&lt;/s&gt;&lt;/li&gt;
&lt;li&gt;元素事件不要直接写在组件上，而是以&lt;code&gt;element.addEventListener(&apos;event&apos;, function(){})&lt;/code&gt;形式写在 &lt;code&gt;js&lt;/code&gt; 文件里面&lt;/li&gt;
&lt;li&gt;尽量不要使用双引号，用单引号？&lt;/li&gt;
&lt;li&gt;模板字符串&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let name = &apos;j10c&apos;;
console.log(&apos;im &apos; + name);
console.log(`im ${name}`);
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;6&quot;&gt;
&lt;li&gt;箭头函数&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;socket.on(&apos;receiveMessage&apos;, (message) =&amp;gt; {
	console.log(&apos;received a message broadcast message:&apos;, message);
	addMessage(message.sender, message.content);
});
  
// message 是函数的参数，该函数为socket.on(eventName, function(){})函数的一个参数
  
//等价于
socket.on(&apos;receiveMessage&apos;, function(message){
	// code here...
});
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;7&quot;&gt;
&lt;li&gt;如何使用好元素的 &lt;code&gt;classList&lt;/code&gt;，看到 CSS 课上给元素添加 animation 淡入淡出的类标签的时候还挺震撼的，以前一直以为&lt;code&gt;class&lt;/code&gt;和&lt;code&gt;id&lt;/code&gt;本身就是用来区分唯一元素和泛用元素的标签，现在知道了还能把&lt;code&gt;class&lt;/code&gt;看作一种单一的属性。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;主要还是学了socket.io相关知识，期待寒假的时候有机会再练习一遍。&lt;/p&gt;
</content:encoded></item><item><title>手摸手教你用 Taro + React 封装一个antv-f2</title><link>https://site.j10ccc.xyz/zh-cn/blog/antv-f2-taro-zh-cn/</link><guid isPermaLink="true">https://site.j10ccc.xyz/zh-cn/blog/antv-f2-taro-zh-cn/</guid><pubDate>Tue, 07 Mar 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;s&gt;备选标题：用这种方式给 Taro 封装知名可视化库，我一天能写100个&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;有需求要在小程序端展示图表，看了一圈好用的库就两个 echarts 和 antv-f2，echarts感觉是太老了，为了展示的多样性就选择了 antv&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这里提一下，因为图表大多是 canvas 实现，所以微信小程序端选择的时候不会像组件库那样不自由&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;s&gt;去搜了一下，antv有自己的小程序端兼容版，本文终结&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/antvis/wx-f2&quot;&gt;@antvis/wx-f2&lt;/a&gt;，这个说实话，看更新日期就不想用了。他是 antv-f2 的一个分支，应该是做了兼容后就停止开发了。看项目介绍，有繁琐的小程序开发平台配置，和老掉牙的代码写法。考虑到选择 antv 的初衷，就放弃了这种方案&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// wx-f2
// chart 实例通过调用方法进行配置，太落后啦
chart.source(data, {
  date: {
    range: [0, 1],
    type: &apos;timeCat&apos;,
    mask: &apos;MM-DD&apos;
  },
  value: {
    max: 300,
    tickCount: 4
  }
});
chart.area().position(&apos;date*value&apos;).color(&apos;city&apos;).adjust(&apos;stack&apos;);
chart.line().position(&apos;date*value&apos;).color(&apos;city&apos;).adjust(&apos;stack&apos;);
chart.render();
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// 官网的案例
const context = document.getElementById(&apos;container&apos;).getContext(&apos;2d&apos;);
const LineChart = (
  &amp;lt;Canvas context={context} pixelRatio={window.devicePixelRatio}&amp;gt;
    &amp;lt;Chart data={data}&amp;gt;
      &amp;lt;Axis
        field=&amp;quot;date&amp;quot;
        tickCount={3}
        style={{
          label: { align: &apos;between&apos; },
        }}
      /&amp;gt;
      &amp;lt;Axis field=&amp;quot;value&amp;quot; tickCount={5} /&amp;gt;
      &amp;lt;Line x=&amp;quot;date&amp;quot; y=&amp;quot;value&amp;quot; /&amp;gt;
      &amp;lt;Tooltip /&amp;gt;
    &amp;lt;/Chart&amp;gt;
  &amp;lt;/Canvas&amp;gt;
);

const chart = new Canvas(LineChart.props);
chart.render();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;开发小程序用的是 Taro + React，组件开发自然喜欢 jsx 多点，由官网示例可以看出，我们的图表结构完全是由 jsx 描述的，但实际上我们不会把 jsx 渲染出来，而是提取图表的结构信息，给到 chart instance，让他在 canvas 中渲染出来&lt;/p&gt;
&lt;p&gt;但是这种写法有点不好&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;把图表结构当成一个变量了（虽然 antv 渲染图表就是要靠这个），我们习惯把 jsx 写在 return 中&lt;/li&gt;
&lt;li&gt;浏览器提供的 canvas 和 图表 jsx 结构分离，看起来很难受&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我想像平时写组件一样写 f2&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const MyChart = () =&amp;gt; {
  return (
    &amp;lt;F2&amp;gt;
      { /** 折线图 */ }
      { /** 坐标轴 */ }
    &amp;lt;/F2&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;于是，我就开始封装我的 f2 组件了&lt;/p&gt;
&lt;p&gt;首先，照着官网示例，先拿到 canvas 的 context&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { Canvas } from &amp;quot;@tarojs/components&amp;quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const staticConfig = useRef&amp;lt;ChartProps&amp;gt;();

useReady(() =&amp;gt; {
  const query = Taro.createSelectorQuery();
  query.select(`#${chartId}`)
    .fields({node: true, size: true})
    .exec((res) =&amp;gt; {
      const { node, width, height } = res[0];
      const pixelRatio = Taro.getSystemInfoSync().pixelRatio;
      node.width = width * pixelRatio;
      node.height = height * pixelRatio;
      staticConfig.current = {
        context: node.getContext(&amp;quot;2d&amp;quot;),
        pixelRatio,
        height,
        width,
      };
    });
});

return (
  { /** 这个 Canvas 是小程序（Taro）的，不是 antv 的 */ }
  &amp;lt;Canvas
    type=&amp;quot;2d&amp;quot;
    canvasId={chartId}
    id={chartId}
    style={{ width: &amp;quot;100%&amp;quot;, height: &amp;quot;100%&amp;quot; }}
  /&amp;gt;
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码还是很多坑的，首先拿 dom 要在 &lt;code&gt;Taro.useReady&lt;/code&gt; 生命周期中拿，Taro 的 canvas 组件需要传入&lt;code&gt;id&lt;/code&gt;，&lt;code&gt;canvasId&lt;/code&gt;倒是可以不用（不知道为什么Taro文档没说&lt;code&gt;id&lt;/code&gt;是必要的，我以为他把&lt;code&gt;canvasId&lt;/code&gt;编译成&lt;code&gt;id&lt;/code&gt;了，导致我后面一直拿不到 dom），保险起见我们两个都写。考虑到一个页面有多个图表，我们希望上层组件由用户传入一个&lt;code&gt;id&lt;/code&gt;，来保证 &lt;code&gt;id&lt;/code&gt; 唯一（其实可以用 uuid 实现）&lt;/p&gt;
&lt;p&gt;可以看到 antv 官方示例中 给 canvas instance 传入的是 jsx 对象，其中包含 canvas context，我们这边已经拿到了 context 和其他的 canvas dom 信息，就直接保存到 &lt;code&gt;staticConfig&lt;/code&gt;中，方便以后使用这些静态配置&lt;/p&gt;
&lt;p&gt;有 canvas 配置了，我们就可以去获取图表配置了&lt;/p&gt;
&lt;p&gt;我们希望以这种形式使用 F2&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;export default () =&amp;gt; {
  return (
    &amp;lt;F2 chartId=&amp;quot;mychart-01&amp;quot;&amp;gt;
      &amp;lt;Canvas&amp;gt;
        &amp;lt;Chart data={data} scale={scale} &amp;gt;
          &amp;lt;Axis
            field=&amp;quot;label&amp;quot;
            style={{ label: { align: &amp;quot;between&amp;quot; } }}
          /&amp;gt;
          &amp;lt;Axis field=&amp;quot;sum&amp;quot; /&amp;gt;
          &amp;lt;Line x=&amp;quot;label&amp;quot; y=&amp;quot;sum&amp;quot; lineWidth=&amp;quot;4px&amp;quot; shape=&amp;quot;smooth&amp;quot; style={{ stroke: &amp;quot;#29cf74&amp;quot;}}/&amp;gt;
          &amp;lt;Point x=&amp;quot;label&amp;quot; y=&amp;quot;sum&amp;quot; color=&amp;quot;#009c50&amp;quot; /&amp;gt;
        &amp;lt;/Chart&amp;gt;
      &amp;lt;/Canvas&amp;gt;
    &amp;lt;/F2&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那我们就从&lt;code&gt;props.children&lt;/code&gt;中拿表的配置&lt;code&gt;dynamicConfig&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const { props: dynamicConfig } = props.children;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;至此我们图表的所有配置都拿到了，现在就是渲染了，先创建一个 chart 实例，给他存到 ref 中，因为以后更新图表还要用到它，这里要注意⚠️，更新表单要用 update，而创建图表用 render，以 ref 为空来判断状态&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { Canvas as AntVCanvas } from &amp;quot;@antv/f2&amp;quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const chartRef = useRef&amp;lt;AntVCanvas&amp;gt;();

const renderChart = (config: ChartProps) =&amp;gt; {
  if (chartRef.current) {
    chartRef.current.update(config);
  } else {
    chartRef.current = new AntVCanvas(config);
    chartRef.current.render();
  } 
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们希望图表在挂载时渲染一次，之后 children 更新后都是 update&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const [isReady, setIsReady] = useState(false);

useEffect(() =&amp;gt; {   
  if (!isReady) return;
  const { props: dynamicConfig } = children;
  renderChart({ ...dynamicConfig, ...staticConfig.current});
}, [children, isReady]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同时我们注意到，如果 context 没找到，即 &lt;code&gt;config&lt;/code&gt; 中没有 context（&lt;code&gt;onReady&lt;/code&gt; 在 &lt;code&gt;useEffect&lt;/code&gt;后执行），我们一尝试渲染必报错，于是我们在找到 dom 后 &lt;code&gt;setIsReady&lt;/code&gt;，同时将 &lt;code&gt;isReady&lt;/code&gt; 给到 dep list，就能解决问题了&lt;/p&gt;
&lt;p&gt;到此封装结束，亮出代码✨&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import { Canvas } from &amp;quot;@tarojs/components&amp;quot;;
import Taro, { useReady } from &amp;quot;@tarojs/taro&amp;quot;;
import { useState, useEffect, useRef } from &amp;quot;react&amp;quot;;
import { Canvas as AntVCanvas } from &amp;quot;@antv/f2&amp;quot;;
import { ChartProps } from &amp;quot;@antv/f2/es/canvas&amp;quot;;

type PropsType = {
  chartId: string;
  children: JSX.Element;
}

const F2 = (props: PropsType) =&amp;gt; {
  const staticConfig = useRef&amp;lt;ChartProps&amp;gt;();
  const chartRef = useRef&amp;lt;AntVCanvas&amp;gt;();
  const [isReady, setIsReady] = useState(false);
  const { children, chartId } = props;

  useReady(() =&amp;gt; {
    const query = Taro.createSelectorQuery();
    query.select(`#${chartId}`)
      .fields({node: true, size: true})
      .exec((res) =&amp;gt; {
        const { node, width, height } = res[0];
        const pixelRatio = Taro.getSystemInfoSync().pixelRatio;
        node.width = width * pixelRatio;
        node.height = height * pixelRatio;
        staticConfig.current = {
          context: node.getContext(&amp;quot;2d&amp;quot;),
          pixelRatio,
          height,
          width,
        };
        setIsReady(true);
      });
  });

  const renderChart = (config: ChartProps) =&amp;gt; {
    if (chartRef.current) {
      chartRef.current.update(config);
    } else {
      chartRef.current = new AntVCanvas(config);
      chartRef.current.render();
    }
  };

  useEffect(() =&amp;gt; {
    if (!isReady) return;
    const { props: dynamicConfig } = children;
    renderChart({ ...dynamicConfig, ...staticConfig.current});
  }, [children, isReady]);

  return (
    &amp;lt;Canvas
      type=&amp;quot;2d&amp;quot;
      canvasId={chartId}
      id={chartId}
      style={{ width: &amp;quot;100%&amp;quot;, height: &amp;quot;100%&amp;quot; }}
    /&amp;gt;
  );
};

export default F2;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于小程序的 context 不是标准的（有些功能不被 antv 兼容），可以用&lt;a href=&quot;https://github.com/antvis/f2-context&quot;&gt;官方的案例&lt;/a&gt;来给 canvas context 抹平差异&lt;/p&gt;
&lt;p&gt;最后吐槽一下 antv 的示例页面，cpu占用很高，电脑烫手了我才反应过来，体验很差😭&lt;/p&gt;
</content:encoded></item><item><title>武装到牙齿的 Vim - React配置篇</title><link>https://site.j10ccc.xyz/zh-cn/blog/armed-vim-zh-cn/</link><guid isPermaLink="true">https://site.j10ccc.xyz/zh-cn/blog/armed-vim-zh-cn/</guid><pubDate>Tue, 21 Jun 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://cdn.j10ccc.xyz/static/blog/jrcBy4.png&quot; alt=&quot;jrcBy4.png&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Feature&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;JSX / TSX 语法检测(TSServer/ESLint)，不亚于 VSCode 的补全体验&lt;/li&gt;
&lt;li&gt;方便且全面快捷键操作，包括 Buffer, Tab, Window 切换，Tmux window 创建与切换&lt;/li&gt;
&lt;li&gt;完整的文件管理方案&lt;/li&gt;
&lt;li&gt;Markdown 的深度适配&lt;/li&gt;
&lt;li&gt;其他语言代码有基本的编辑器能力支持&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;tmux&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;# set -g default-terminal &amp;quot;xterm-256color&amp;quot;
# set -ga terminal-overrides &amp;quot;,*256col*:Tc&amp;quot;
# set -ga terminal-overrides &amp;quot;,xterm-256color:Tc&amp;quot;
# action key
**
unbind C-b
set-option -g prefix C-t
set-option -g repeat-time 0

# vim-like pane switching
bind -r k select-pane -U
bind -r j select-pane -D
bind -r h select-pane -L
bind -r l select-pane -R

# 
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;C-T&lt;/code&gt; + &lt;code&gt;P(N)&lt;/code&gt; switch previous (next) window&lt;/li&gt;
&lt;li&gt;&lt;code&gt;C-S-Left(Right)&lt;/code&gt; move window in status bar&lt;/li&gt;
&lt;li&gt;&lt;code&gt;C-T&lt;/code&gt; + &lt;code&gt;C&lt;/code&gt;  create window&lt;/li&gt;
&lt;li&gt;&lt;code&gt;C-T&lt;/code&gt; + &lt;code&gt;H(JKL)&lt;/code&gt; swich focus&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>ZJUT-PC端闪讯与内网共存</title><link>https://site.j10ccc.xyz/zh-cn/blog/china-telecom-yes-zh-cn/</link><guid isPermaLink="true">https://site.j10ccc.xyz/zh-cn/blog/china-telecom-yes-zh-cn/</guid><pubDate>Sat, 11 Sep 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;背景&lt;/h2&gt;
&lt;p&gt;看到朵朵里面好多人在商量电信转移动的事情，心里很难受。要知道校外电信口碑碾压移动，在校内，一次选课后，电信怎么就爆炸了呢？&lt;/p&gt;
&lt;p&gt;跨专业学习了一下，终于搓出了这份教程💦💦💦&lt;/p&gt;
&lt;h2&gt;约定&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;你的电脑应该至少有两张网卡，一张有线，一张无线（说人话就是能既插网线又能连WIFI，拓展坞实现插网线的也可以，一般的笔记本都能满足要求）&lt;/li&gt;
&lt;li&gt;本教程适用于Windows系统，在 Windows10 21H1 和 20H2版本上已经成功，MacOS 也能实现，把 Powershell 的命令转译成 bash 就行了，当然欢迎 Linux 用户来捧场，会手动拨号了改个路由表应该不在话下。&lt;/li&gt;
&lt;li&gt;对于已经使用了 OpenWrt 通过 WIFI 访问外网的电信用户，在客户端上你需要&lt;strong&gt;两张&lt;/strong&gt;无线网卡，也可以在路由器上按教程中的原理配置。&lt;/li&gt;
&lt;li&gt;嫌麻烦的看完&lt;strong&gt;单独访问内网&lt;/strong&gt;就可以结束了，以后用内网先要断闪讯，插移动专用网口，或者连 &lt;strong&gt;Wlan-edu&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;有什么问题邮箱（网页底部）找我，看到了就回。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;单独访问内网&lt;/h2&gt;
&lt;p&gt;先从寝室里单独使用内网说起吧，好多人 &lt;s&gt;（助班）&lt;/s&gt; 都不知道电信用户能在寝室访问内网，电脑有两种方式，手机有一种：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;电脑断开所有网络的有线连接（闪讯）和无线连接，WIFI 连接 &lt;strong&gt;Wlan-edu&lt;/strong&gt;，随便访问一个内网 IP（如&lt;code&gt;192.168.1.1&lt;/code&gt;），会出现登录页面，输入账号密码，选择校园网，登录成功即可。&lt;s&gt;这样子网速稍微慢一点&lt;/s&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;电脑插墙壁上比较新的网口（就是移动用户上网插的网口），随便访问一个内网 IP（如&lt;code&gt;192.168.1.1&lt;/code&gt;），会跳转到登录页面，登录成功即可。&lt;s&gt;这样访问内网最快的！&lt;/s&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;手机不需要关闭流量开关，WIFI 连接 &lt;strong&gt;Wlan-edu&lt;/strong&gt;，随便访问一个内网 IP（如&lt;code&gt;192.168.1.1&lt;/code&gt;），会出现登录页面，登录成功即可。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;第一次登录宿舍楼（梦溪）校园网&lt;code&gt;Wlan-edu&lt;/code&gt;，账号是学号，密码是身份证后8位，登录成功会提示去自助服务平台开通，在 Dr.COM 那个页面输入同样的账号密码，成功登录后会发现账号状态默认停机，右侧业务办理处点击“用户注册开通”链接，弹出什么责任书，同意然后输入短信验证码，就登录成功了，接下来弹出一个页面，里面有两个选项，不用管他，回到Dr.com这个页面，刷新一下，看到账户状态是在线的就行了，然后就能访问内网了。 &lt;strong&gt;图文可见 &lt;a href=&quot;https://mp.weixin.qq.com/s/DIup5mkQXN5GCoYkn5ef6w&quot;&gt;小和山校园网 9 月 11 日发表的文章&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;第一次登录教学区校园网&lt;code&gt;Zjut-stu&lt;/code&gt;，账号是学号，密码是身份证后8位，登录成功会提示去自助服务平台开通，此时的办理页面不再是 Dr.COM 了，点击办理，出现“已处理”提示，但只停留在&lt;strong&gt;步骤一&lt;/strong&gt;，无法继续执行。正确的方式应是去&lt;strong&gt;工大企业号（微门户）办事大厅&lt;/strong&gt;申请 &lt;strong&gt;“学生校园网账号办理”&lt;/strong&gt; ，此时你的手机应该只打开了流量开关，而 WIFI 没有连接&lt;code&gt;Zjut-stu&lt;/code&gt;,一样地，一路选择下一步，最后会弹出一个无法加载的页面（显示域名为&lt;code&gt;172&lt;/code&gt;开头的网页无法加载），此时&lt;strong&gt;断开流量开关&lt;/strong&gt;，WIFI 连接&lt;code&gt;Zjut-stu&lt;/code&gt;，微信右上角&lt;strong&gt;刷新页面&lt;/strong&gt;，即可加载成功。接下来右侧业务办理处点击“用户注册开通”链接，弹出什么责任书，同意然后输入短信验证码，就开通成功了。最后再重新连接&lt;code&gt;Zjut-stu&lt;/code&gt;，手机自带登录页面重新登录一次即可。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;同时访问内外网&lt;/h2&gt;
&lt;p&gt;在此之前，你应该已经自助服务开通了校园网账号，并且你的&lt;strong&gt;Windows电脑&lt;/strong&gt;应该能&lt;strong&gt;单独访问内网&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;同时使用内网和外网（闪讯），考虑到成本，对于大多数笔记本，理想的连接方式就是内网用 WIFI，外网用闪讯，上面提到了，这样访问内网可能会比较慢（如果有两个有线网卡是最好的）&lt;/p&gt;
&lt;p&gt;先插线连接闪讯，再连接名为 &lt;strong&gt;Wlan-edu&lt;/strong&gt; 的WIFI，&lt;strong&gt;管理员身份&lt;/strong&gt;打开 &lt;strong&gt;Powershell&lt;/strong&gt;，输入 &lt;code&gt;ipconfig&lt;/code&gt;，找到&lt;strong&gt;无线局域网适配器 WLAN&lt;/strong&gt;的默认网关（如果默认网关为空，那就再等一下），也就是校园网的网关，记为&lt;code&gt;${网关IP}&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;再输入&lt;code&gt;route print&lt;/code&gt;，打开&lt;strong&gt;路由表&lt;/strong&gt;，应该会看到类似以下的字样（顺序乱了没事）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-fallback&quot;&gt;IPv4 路由表
===========================================================================
活动路由:
网络目标           网络掩码               网关               接口         跃点数
0.0.0.0           0.0.0.0            在链路上     115.200.94.163            36
0.0.0.0           0.0.0.0         172.28.0.1        172.28.94.29          4261
0.0.0.0           0.0.0.0           ${网关IP}         ${对应接口}          4556
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;原理&lt;/h3&gt;
&lt;p&gt;解释一下三个解析&lt;code&gt;0.0.0.0&lt;/code&gt;的路由，接口&lt;code&gt;115&lt;/code&gt;开头的是 NetKeeper 拨号的路由，&lt;code&gt;172&lt;/code&gt;开头的是有线网卡的路由，他们两个负责解析&lt;strong&gt;内外网 IP&lt;/strong&gt;。下面以&lt;code&gt;10&lt;/code&gt;开头的是无线网卡的路由， 也负责解析内外网IP，因为跃点数越低的路由优先级越高，所以我们输入内网网址&lt;code&gt;192.168.210.112&lt;/code&gt;时，负责解析的网关是 NetKeeper 的网关，那当然解析不到内网主机，下面将内网路由添加到&lt;strong&gt;永久路由&lt;/strong&gt;（也叫静态路由，跃点数为 1，优先级最高），为了防止内网网口解析外网 IP，所以我们指定解析特定的 IP段。&lt;/p&gt;
&lt;h3&gt;具体实现&lt;/h3&gt;
&lt;p&gt;输入&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;route delete 0.0.0.0 mask 255.255.128.0 ${对应接口}
#三个网关冲突了，所以选择删除内网路由
#注意中间空格
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面添加确定的 IP，专门使用内网连接&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;route -p add 192.168.210.0 mask 255.255.255.0 ${网关IP}
# ${ip} 为网关 IP，请根据实际修改
# -p 是添加到永久路由的参数，拉高优先级，同时防止电脑重启后需要手动重新配置
#内网登录的 IP 为 192.168.210.112，以 192.168.210.0 为网络号有效避免与电脑上其他内网服务擦枪走火
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然还要添加&lt;code&gt;172&lt;/code&gt;开头的内网 IP，因为选课的 IP 为&lt;code&gt;172.16.19.163&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;route -p add 172.0.0.0 mask 255.0.0.0 ${网关IP}
#这边没有考虑其他网卡需要访问172开头的IP，我寻思也用不到啊，如果需要，按照上面的步骤修改命令
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面添加的路由都会在&lt;strong&gt;路由表&lt;/strong&gt;里面显示，如果觉得需要再次修改（删除再添加），请执行以下命令&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;route -p delete ${负责解析的网络号/IP} mask ${子网掩码} ${网关IP}
#删除路由时三个参数要跟路由表里面的其中一条路由对应上，不然命令会执行失败
#含-p 是删除永久路由，如果要删除活动路由，则不需要加 -p
route -p add ${负责解析的网络号/IP} mask ${子网掩码} ${网关IP} #添加你想要的路由
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后去访问一下内网登录网页&lt;code&gt;192.168.210.112&lt;/code&gt;，应该是能访问了。&lt;/p&gt;
&lt;p&gt;以上只添加了&lt;code&gt;192.168.210&lt;/code&gt;和&lt;code&gt;172.0.0.0&lt;/code&gt;两个网络号，如果以后有内网的 IP 属于其他的网络号的话，手动添加到路由表里面&lt;/p&gt;
&lt;p&gt;如果以后网关 IP 改变了，也需要修改路由表，不过网关应该是不会改的。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;电信在操作便捷性（定期换密码，手动设置内网）这块确实干不过移动😅，&lt;s&gt;移动跟学校有py&lt;/s&gt;，但是其他方面是碾压的（延迟和实际网速）&lt;/p&gt;
</content:encoded></item><item><title>对Windows终端的新宋体说拜拜</title><link>https://site.j10ccc.xyz/zh-cn/blog/cmd-font-zh-cn/</link><guid isPermaLink="true">https://site.j10ccc.xyz/zh-cn/blog/cmd-font-zh-cn/</guid><pubDate>Thu, 06 Feb 2020 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;背景&lt;/h2&gt;
&lt;p&gt;这几天因为疫情，学校在网上开课，但我又刚买了2只舵机，得在树莓派上先搭建好环境，于是就一边上课一边搭建，因为网课限制Windows平台，所以使用&lt;code&gt;git&lt;/code&gt;连到了树莓派，但是我发现了一个严重的问题——cmd的字体不是等宽的！&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.j10ccc.xyz/static/blog/39VeL6.png&quot; alt=&quot;39VeL6.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;默认的新宋体&lt;/p&gt;
&lt;p&gt;不得不承认的是，在中文环境下，PowerShell 默认 的「新宋体」确实很（zhen）不（ta）耐（ma）看（chou）。然而由于默认 PowerShell 终端是一个非常底层的应用，其界面甚至没有使用通用 UI 渲染框架来实现，而是直接调用底层 Windows API 来实现，因此其字体要求非常严格。这也是我们不能将任意一个等宽字体替换上去的原因。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The fonts must meet the following criteria to be available in a command session window:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The font must be a fixed-pitch font.&lt;/li&gt;
&lt;li&gt;The font cannot be an italic font.&lt;/li&gt;
&lt;li&gt;The font cannot have a negative A or C space.&lt;/li&gt;
&lt;li&gt;If it is a TrueType font, it must be FF_MODERN.&lt;/li&gt;
&lt;li&gt;If it is not a TrueType font, it must be OEM_CHARSET. Additional criteria for Asian installations:&lt;/li&gt;
&lt;li&gt;If it is not a TrueType font, the face name must be “Terminal.”&lt;/li&gt;
&lt;li&gt;If it is an Asian TrueType font, it must also be an Asian character set.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;微软对 cmd / Powershell 字体的限制&lt;/p&gt;
&lt;h2&gt;两种字体&lt;/h2&gt;
&lt;p&gt;虽然要求很严格，但仍然有一两款比较好的字体能兼容 Powershell / cmd ：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/yakumioto/YaHei-Consolas-Hybrid-1.12&quot;&gt;Microsoft YaHei Mono&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/be5invis/Sarasa-Gothic/releases&quot;&gt;更纱黑体&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Microsoft YaHei Mono 是微软为 &lt;a href=&quot;https://www.microsoft.com/zh-cn/p/ubuntu/9nblggh4msv6?activetab=pivot:overviewtab#&quot;&gt;Ubuntu on Windows&lt;/a&gt; 设计的一款等宽字体，正好可以拿来给自家的Powershell用，但是它渲染的汉字却有些奇怪。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.j10ccc.xyz/static/blog/39VneK.png&quot; alt=&quot;39VneK.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;Microsoft YaHei Mono&lt;/p&gt;
&lt;p&gt;更纱黑体较圆润的 Microsoft YaHei Mono，更为修长。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.j10ccc.xyz/static/blog/39VudO.png&quot; alt=&quot;39VudO.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;更纱黑体&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.j10ccc.xyz/static/blog/39VZsx.png&quot; alt=&quot;39VZsx.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;两种字体在同字号（28）下渲染汉字对比&lt;/p&gt;
&lt;p&gt;仔细看可以发现 Microsoft YaHei Mono 因为渲染的字符相对较大，所以显示得更清晰，而更纱黑体却有些发虚（1080P截图），但对汉字的结构尺寸方面更纱黑体控制得更好，Microsoft YaHei Mono 渲染的汉字笔画粗细不均。&lt;/p&gt;
&lt;h2&gt;评语&lt;/h2&gt;
&lt;p&gt;个人更喜欢更纱黑体，不喜欢 Microsoft YaHei Mono 对汉字的渲染。&lt;/p&gt;
&lt;p&gt;如果有觉得Windows下字体发虚想修改系统的全局字体（微软雅黑）的，建议屏幕&lt;strong&gt;2K分辨率以上&lt;/strong&gt;的使用 MacType。&lt;s&gt;1080P的就省省吧，越改越发虚&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;最后讲个笑话，微软自己出的 Windows Terminal，应用商店的软件截图中，Terminal 使用的字体是 FiraCode 。。&lt;/p&gt;
</content:encoded></item><item><title>面向团队的前端代码规范</title><link>https://site.j10ccc.xyz/zh-cn/blog/code-convention-fe-zh-cn/</link><guid isPermaLink="true">https://site.j10ccc.xyz/zh-cn/blog/code-convention-fe-zh-cn/</guid><pubDate>Tue, 21 Jun 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;说在前面&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;本文的代码风格为笔者偏好，用于笔者所在的团队，
文章仅提供配置思路和选择项参考&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;规范基于 ES6 语法，可能与一些现有的项目不匹配&lt;/p&gt;
&lt;p&gt;前端规范要 5 个插件：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;ESLint&lt;/li&gt;
&lt;li&gt;Prettier&lt;/li&gt;
&lt;li&gt;commitLint&lt;/li&gt;
&lt;li&gt;styleLint&lt;/li&gt;
&lt;li&gt;husky&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;以及一个配置文件：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;.editorconfig&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;不同编辑器可能需要插件等一些手段来支持这些工具，
VSCode 配置比较方便，ESLint, Prettier, editorconfig 安装插件即可&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;后期团队内可能会构建一个脚手架，方便直接安装必要的包，&lt;s&gt;现在还是老老实实复制粘贴代码吧&lt;/s&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;必要的依赖&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;&amp;quot;devDependencies&amp;quot;: {
  &amp;quot;eslint&amp;quot;: &amp;quot;^8.0.1&amp;quot;,
  &amp;quot;eslint-config-prettier&amp;quot;: &amp;quot;^8.5.0&amp;quot;,
  &amp;quot;eslint-config-standard&amp;quot;: &amp;quot;^17.0.0&amp;quot;,
  &amp;quot;eslint-plugin-import&amp;quot;: &amp;quot;^2.25.2&amp;quot;,
  &amp;quot;eslint-plugin-n&amp;quot;: &amp;quot;^15.0.0&amp;quot;,
  &amp;quot;eslint-plugin-prettier&amp;quot;: &amp;quot;^4.0.0&amp;quot;,
  &amp;quot;eslint-plugin-promise&amp;quot;: &amp;quot;^6.0.0&amp;quot;,
  &amp;quot;eslint-plugin-react&amp;quot;: &amp;quot;^7.29.4&amp;quot;,
  &amp;quot;prettier&amp;quot;: &amp;quot;^2.6.2&amp;quot;,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;ESLint&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// .eslintrc.js
module.exports = {
  env: {
    browser: true,
    es2021: true
  },
  extends: [&amp;quot;standard&amp;quot;, &amp;quot;plugin:prettier/recommended&amp;quot;],
  parser: &amp;quot;@typescript-eslint/parser&amp;quot;,
  parserOptions: {
    ecmaFeatures: {
      jsx: true
    },
    ecmaVersion: &amp;quot;latest&amp;quot;,
    sourceType: &amp;quot;module&amp;quot;
  },
  plugins: [&amp;quot;react&amp;quot;, &amp;quot;@typescript-eslint&amp;quot;],
  rules: {
    semi: [&amp;quot;error&amp;quot;, &amp;quot;always&amp;quot;],
    quotes: [1, &amp;quot;double&amp;quot;]
  }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Prettier&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.prettierrc.json&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;printWidth&amp;quot;: 80,
  &amp;quot;tabWidth&amp;quot;: 2,
  &amp;quot;useTabs&amp;quot;: false,
  &amp;quot;semi&amp;quot;: true,
  &amp;quot;bracketSpacing&amp;quot;: true,
  &amp;quot;endOfLine&amp;quot;: &amp;quot;lf&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.prettierignore&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;# Ignore artifacts:
build
coverage

# Ignore all HTML files:
*.html
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;editorconfig&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-editorconfig&quot;&gt;root = true

[*]
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 2
charset = utf-8
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>用 Docker 高效部署前端应用</title><link>https://site.j10ccc.xyz/zh-cn/blog/docker-deploy-webpages-zh-cn/</link><guid isPermaLink="true">https://site.j10ccc.xyz/zh-cn/blog/docker-deploy-webpages-zh-cn/</guid><pubDate>Sun, 21 Aug 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;废话写在前头&lt;/h2&gt;
&lt;p&gt;之前写 &lt;a href=&quot;https://github.com/SummersDays/brisk-tab&quot;&gt;Brisk-Tab&lt;/a&gt; 的时候，就有把个人的页面部署到服务器上，并经常访问，当时的方案是 &lt;strong&gt;宝塔 + Nginx&lt;/strong&gt; 。&lt;s&gt;因为操作傻瓜才用的，而且当时也不太懂运维这块&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;宝塔里面的 Nginx 一言难尽，部署 Node 应用成功与否还要看运气。近期又有一些项目要部署，刚好一个 hxd 前段时间接触过 Nginx，于是我就把服务器格掉，让他来帮我部署。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;结果这老司机在我的服务器上翻车了😂&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;好吧，我不装了，其实我之前一直在看前端工程化的案例，&lt;s&gt;但是迟迟没有动手操作。&lt;/s&gt;
Docker 使用 Nginx 镜像，做到上线环境和本地环境分离，这样就没有一些奇奇怪怪的文件权限问题了（老司机跟我解释这个是失败原因）&lt;/p&gt;
&lt;p&gt;于是昨天晚上到了一个项目的 ddl 的前一天晚上，我就滚去学了 Docker。&lt;/p&gt;
&lt;h2&gt;故事开始&lt;/h2&gt;
&lt;h3&gt;先上教程&lt;/h3&gt;
&lt;p&gt;一上手我就去  Google 上一顿乱搜。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Docker 中有几个概念：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;镜像（Image）&lt;/strong&gt;：Docker 镜像（Image），就相当于是一个 root 文件系统。比如官方镜像 ubuntu:16.04 就包含了完整的一套 Ubuntu16.04 最小系统的 root 文件系统。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;容器（Container）&lt;/strong&gt;：镜像（Image）和容器（Container）的关系，就像是面向对象程序设计中的类和实例一样，镜像是静态的定义，容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;仓库（Repository）&lt;/strong&gt;：仓库可看成一个代码控制中心，用来保存镜像。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;先在本地装个 Docker 啦（本文以 macos 演示）&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# macos
brew install docker

sudo docker --version
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;记得每次在命令行中启动容器前，先要打开 Docker.app&lt;/p&gt;
&lt;p&gt;顺便在要部署的服务器（Linux）上也装一个&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# linux 不限发行版 
curl -fsSL get.docker.com -o get-docker.sh
sh get-docker.sh

sudo docker --version
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;然后在 React 项目根目录写个 &lt;code&gt;nginx.conf&lt;/code&gt;。这是 每个镜像中 Nginx 的配置&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-nginx&quot;&gt;server {
  listen 80;
  server_name localhost;

  access_log /var/log/nginx/host.access.log  main;
  error_log  /var/log/nginx/error.log  error;

  location / {
    root   /usr/share/nginx/html;
    index index.html index.htm
    try_files $uri $uri/ /index.html;
  }

  # error_page  404              /404.html;
  
  error_page   500 502 503 504  /50x.html;
  location = /50x.html {
    root   /usr/share/nginx/html;
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;接下来要让 Docker 生成我们想要的镜像。先编写个 &lt;code&gt;Dockerfile&lt;/code&gt;，写前端镜像构建的指令&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;FROM node:16 # 第一阶段
WORKDIR /app
COPY . /app
RUN yarn &amp;amp;&amp;amp; yarn build

FROM nginx # 第二阶段
COPY --from=0 /app/dist /usr/share/nginx/html
COPY --from=0 /app/nginx.conf /etc/nginx/conf.d/default.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编写 Dockerfile 有几个要注意的点&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;FROM&lt;/code&gt;表示从官方仓库中使用镜像，本项目要用到 node 和 nginx 镜像来确保应用生产环境的运行依赖&lt;/li&gt;
&lt;li&gt;&lt;code&gt;COPY&lt;/code&gt; 指令将当前文件夹拷贝到镜像的 &lt;code&gt;/app&lt;/code&gt; 下&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RUN&lt;/code&gt; 指令在 docker build 时运行，每条 RUN 指令都会在 docker 上新建一层，这样会导致镜像的体积过大，所以代码中将两条 yarn 语句合并成一条&lt;/li&gt;
&lt;li&gt;&lt;code&gt;COPY --from 0&lt;/code&gt; 表示把第一阶段编译好后的文件和 nginx.conf 复制到镜像的 nginx目录下方便调用&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;之后我们开始构建镜像&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;sudo docker build -t app-name-image .
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-t&lt;/code&gt; 用来制定镜像名称&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;5&quot;&gt;
&lt;li&gt;最后使用改镜像生成一个容器，并将&lt;strong&gt;容器&lt;/strong&gt;的 80 端口（&lt;code&gt;nginx.conf&lt;/code&gt;中监听的就是80端口）映射到本地的 一个端口，这样访问本地的端口就能直接访问到容器中的应用了&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;sudo docker run -itd -p 8081:80 --name app-name-container app-name-image
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-itd&lt;/code&gt; 使 docker 在后台运行&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-p ${本地端口}kk:${容器端口}&lt;/code&gt; 映射端口&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--name&lt;/code&gt; 制定容器名称&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;常用命令&lt;/h3&gt;
&lt;p&gt;除了上文提到的 &lt;code&gt;docker run&lt;/code&gt; 、&lt;code&gt;docker build&lt;/code&gt;，还有一些常用的命令&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;sudo docker ps
# 列出所有正在运行的容器
# 这里可以显示一个容器的ID

sudo docker images
# 列出本地所有的镜像
# 这里可以显示一个镜像的ID

sudo docker stop ${container-id}
# 停止运行一个容器

sudo docker rm ${images-id}
# 删除某个镜像，但在删除前需要停止一个容器
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;给服务配上域名&lt;/h2&gt;
&lt;p&gt;众所周知，通过域名访问 HTTP 服务是默认访问80端口，但是我们的服务对应的是本地的8081端口，这样要通过&lt;strong&gt;域名+端口&lt;/strong&gt;的方式才能访问到服务。这里可以在本地的 Nginx 上做一次端口转发，将特定的域名转发到指定的端口。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nginx&quot;&gt;# 宿主机的 conf.d/default.conf
server {
  listen 80;
  server_name site.example.com;

  location / {
    proxy_pass http://127.0.0.1:8081;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-nginx&quot;&gt;# https 服务

# 宿主机的 conf.d/default.conf
server {
  listen 80;
  server_name site.example.com;
  # 运行在本地就是 localhost

  return 301 https://$server_name$request_uri;
  # 这行的变量是通过 访问地址 自动解析的，原封不动抄下来就好
}

server {
  listen 443 ssl;
  server_name site.example.com;

  ssl_certificate /etc/nginx/cert/ssl_file.pem;
  ssl_certificate_key /etc/nginx/cert/ssl_file.key;

  location / {
    proxy_pass http://127.0.0.1:8081;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;若有用户第一次不加协议访问域名（HTTP），则转发到 301 接口，自动使用 HTTPS 协议&lt;/li&gt;
&lt;li&gt;HTTPS 服务默认访问 443 端口，第二个 server 里面做相应的配置&lt;/li&gt;
&lt;li&gt;记得给 监听 443 端口的 server 添加证书路径&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;解决的疑问&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;之前问老司机：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;为什么域名访问 80 端口，服务器能提供不同的服务？&lt;/li&gt;
&lt;li&gt;不同的服务都开在 80 端口，不会端口冲突吗？&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;老司机： 为什么你要问这么可(sha)爱(bi)的问题？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;之前用宝塔，接触的是虚假的运维，当然啥都不懂。现在接触了 原生的 Nginx 之后，有点明白了&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;域名访问的 80 端口，实际上是访问了 Nginx 服务，宿主机运行着一个 Nginx，这个Nginx 通过你编写的配置文件，监听着 80 端口，当一个特定的域名访问了 80 端口，Nginx 会根据配置文件转发到另外一个端口。&lt;/li&gt;
&lt;li&gt;80 端口只开着一个Nginx，当然不会冲突，其他端口同理。&lt;/li&gt;
&lt;li&gt;Docker 容器映射的本地端口不能冲突&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;大佬的反问&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;当我搞定一切后，老司机还是不服气之前的翻车。&lt;/p&gt;
&lt;p&gt;“我上次给你配的 纯 Nginx 部署前端，不比你这个方便？你非要兜个圈子搞套 Docker 来配置？？？小🔥汁玩的挺花啊。。。“&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我不紧不慢得回答道：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;你服务器的环境能保证每个项目都编译成功？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;你是不是新部署个项目就要加一个 server 重新填路径？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;你是不是修改一次配置就要重启 Nginx？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;上次因为不明原因翻车的是谁？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;s&gt;老司机没坑声，但是我知道他还是不服&lt;/s&gt;&lt;/p&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;https://juejin.cn/post/7122708049122459662&lt;/li&gt;
&lt;li&gt;https://github.com/shfshanyue/simple-deploy&lt;/li&gt;
&lt;li&gt;https://www.runoob.com/docker/docker-tutorial.html&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Fiddler | 抓取学习平板上的作业答案</title><link>https://site.j10ccc.xyz/zh-cn/blog/hack-yxy-zh-cn/</link><guid isPermaLink="true">https://site.j10ccc.xyz/zh-cn/blog/hack-yxy-zh-cn/</guid><pubDate>Fri, 13 Mar 2020 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;本文所有涉及个人隐私内容均已打码，如有观看不便，敬请谅解。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;背景&lt;/h2&gt;
&lt;p&gt;&lt;s&gt;这一切得从一只蝙蝠说起。&lt;/s&gt; 学校上学期发了一个三星学习平板，装载着与 &lt;strong&gt;知名教育公司&lt;a href=&quot;https://baike.baidu.com/item/%E5%AD%A6%E6%B5%B7%E6%95%99%E8%82%B2/5666012?fr=aladdin&quot;&gt;学海&lt;/a&gt;&lt;/strong&gt; 深度定 (p) 制 (y) 的 &lt;a href=&quot;https://www.zjxhedu.com/&quot;&gt;”智通云系统“&lt;/a&gt; ，网课作业都在这上面布置，并且还在这上面举办大型考试， &lt;s&gt;这让我这天天上课摸鱼的烂头怎么活？&lt;/s&gt; 当第一次考试失利后，我突然想到可以用 Fiddler 去获取电子试卷的题目，说不定连答案也可以抓到！抱着尝试的心态，我点开了 考场APP&lt;strong&gt;云作业&lt;/strong&gt; …&lt;/p&gt;
&lt;h2&gt;准备&lt;/h2&gt;
&lt;p&gt;Fiddler 4，学习平板一台&lt;/p&gt;
&lt;h2&gt;环境配置&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;打开Fiddler，设置代理端口。 &lt;img src=&quot;https://cdn.j10ccc.xyz/static/blog/8usH54.png&quot; alt=&quot;8usH54.png&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;依次点开开学习平板&lt;code&gt;设置-连接-WLAN-当前网络-高级设置-代理服务器-无—&amp;gt;手动&lt;/code&gt; 代理主机名填运行 Fiddler 的电脑的内网IP，端口填&lt;code&gt;8888&lt;/code&gt;，点击保存。 这时候在平板上随便打开一个APP，刷新两下看看Fiddler里面有没有请求或者响应信息出现。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;开始抓包&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;平板HOME 界面长按 云作业APP ，点击&lt;strong&gt;清除数据&lt;/strong&gt;，清除完成后，打开云作业，同时密切关注 Fiddler 的请求列表。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当云作业首页加载得差不多的时候，一次双击点开 Fiddler 中各个数据包，选择右侧窗口的 &lt;code&gt;TextView&lt;/code&gt; 查看每个数据包的详细内容，如果发现有一个数据包的内容特别长（由滚动条可以看出）那么它就是我们要找的那个包，点击右下角的&lt;code&gt;View in Notepad&lt;/code&gt;，保存这条数据信息。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.j10ccc.xyz/static/blog/8usLG9.png&quot; alt=&quot;8usLG9.png&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;分析数据&lt;/h2&gt;
&lt;p&gt;对这条数据仔细分析一下，不难发现里面有很多已经出现在云作业APP中的作业名称，而且几乎每个作业信息的格式都是一样的。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;   {
     &amp;quot;workId&amp;quot;: &amp;quot;${WORKID}&amp;quot;,
     &amp;quot;studentWorkId&amp;quot;: &amp;quot;${STUDENTWORDID}&amp;quot;,
     &amp;quot;teacherId&amp;quot;: &amp;quot;${TEACHERID}&amp;quot;,
     &amp;quot;haveSeen&amp;quot;: 0,
     &amp;quot;name&amp;quot;: &amp;quot;${NAME}&amp;quot;, //作业名称
     &amp;quot;postscript&amp;quot;: &amp;quot;${POSTSCRIPT}&amp;quot;,
     &amp;quot;createTime&amp;quot;: &amp;quot;${CREATEIME}&amp;quot;, //单位:ms
     &amp;quot;updateTime&amp;quot;: &amp;quot;${UPDATETIME}&amp;quot;,
     &amp;quot;uptoTime&amp;quot;: &amp;quot;${UPTOTIME}&amp;quot;,
     &amp;quot;subject&amp;quot;: 1,
     &amp;quot;schedule&amp;quot;: 1,
     &amp;quot;period&amp;quot;: 103,
     &amp;quot;score&amp;quot;: 0.0,
     &amp;quot;selfScore&amp;quot;: 0.0,
     &amp;quot;emendNum&amp;quot;: 0,
     &amp;quot;contentUrl&amp;quot;: &amp;quot;http://xhfs2.oss-cn-hangzhou.aliyuncs.com${ADRESS}.txt&amp;quot;,
     //此处省略多行
     &amp;quot;inputAssociate&amp;quot;: 1,
     &amp;quot;preDownloadUrls&amp;quot;: null,
     &amp;quot;publishTime&amp;quot;: 0
   }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我抓的明明是作业的信息，可是为什么一个题目都没有显示？&lt;/p&gt;
&lt;p&gt;令人好奇的是，每条作业信息中&lt;code&gt;contentUrl&lt;/code&gt;属性值为一个 URL ，指向的是一个 TXT 文件，我下载来一看&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.j10ccc.xyz/static/blog/8usxr6.jpg&quot; alt=&quot;8usxr6.jpg&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;入眼的是满满的答案啊！&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;   {
     &amp;quot;isCustomScore&amp;quot;: false,
     &amp;quot;lectureInfoList&amp;quot;: [],
     &amp;quot;questionAnswers&amp;quot;: [
       {
         &amp;quot;answerContent&amp;quot;: &amp;quot;A&amp;quot;,  //答案
         &amp;quot;answerContentType&amp;quot;: 2,
         &amp;quot;index&amp;quot;: 0,  //此值可以为1,2,3...表示同一题的多个答案
         &amp;quot;inputType&amp;quot;: 1,
         &amp;quot;keyboardType&amp;quot;: &amp;quot;&amp;quot;,
         &amp;quot;optionNum&amp;quot;: 4,
         &amp;quot;orgIndex&amp;quot;: -1,
         &amp;quot;questionId&amp;quot;: &amp;quot;${QUESTIONID}&amp;quot;,
         &amp;quot;score&amp;quot;: 20000  //分数，单位:10^-4分
       }
       
       //此处省略多行
     ],
     &amp;quot;questionPoolContentInfos&amp;quot;: [
    	{
         &amp;quot;bookId&amp;quot;: &amp;quot;${BOOKID}&amp;quot;,
         &amp;quot;catalogId&amp;quot;: &amp;quot;${CATALOGID}&amp;quot;,
         &amp;quot;checkType&amp;quot;: 0,
         &amp;quot;deleteType&amp;quot;: 0,
         &amp;quot;difficulty&amp;quot;: 0,
         &amp;quot;drawingUrl&amp;quot;: &amp;quot;&amp;quot;,
         &amp;quot;errorRate&amp;quot;: 0,
         &amp;quot;explainContent&amp;quot;: &amp;quot;${EXPLAINCONTENT}&amp;quot;, //题目答案解析
         &amp;quot;explainContentType&amp;quot;: 0,
         &amp;quot;isAllDoFlag&amp;quot;: 1,
         &amp;quot;isEncrypt&amp;quot;: 0,
         &amp;quot;isTitle&amp;quot;: 0,
         &amp;quot;isWrongQuestion&amp;quot;: 0,
         &amp;quot;listeningUrl&amp;quot;: &amp;quot;&amp;quot;,
         &amp;quot;lockInputType&amp;quot;: 0,
         &amp;quot;parentQuestionId&amp;quot;: &amp;quot;${PARENTQUESTIONID}&amp;quot;,
         &amp;quot;questionId&amp;quot;: &amp;quot;${QUESTIONID}&amp;quot;,
         &amp;quot;questionLayoutType&amp;quot;: 1,
         &amp;quot;questionSystemType&amp;quot;: 6,
         &amp;quot;questionUserType&amp;quot;: 6,
         &amp;quot;stemContent&amp;quot;: &amp;quot;${STEMCONTENT}&amp;quot;, //题面
         &amp;quot;stemContentType&amp;quot;: 0,
         &amp;quot;subject&amp;quot;: 2,
         &amp;quot;totalScores&amp;quot;: 40000, 
         &amp;quot;verifyStatus&amp;quot;: 0,
         &amp;quot;workRollVersion&amp;quot;: 0
       }
       //省略多行
      ]
   }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不仅有答案，还有题目和题目解析，看来这个月的作业和考试都不愁了，哈哈哈哈哈&lt;/p&gt;
&lt;h2&gt;导出答案&lt;/h2&gt;
&lt;p&gt;我写了一个 Python3 脚本 :&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# -*- coding: utf-8 -*-

&apos;&apos;&apos;
    运行环境：
    Linux Ubuntu18.04 WSL，需要安装wget
    代码有BUG，多选题答案的各个答案会被当做多道单选题的答案
&apos;&apos;&apos;
import re
import sys
import os,glob

ans = []
subjectlist = [&apos;语文&apos;,&apos;数学&apos;,&apos;英语&apos;,&apos;物理&apos;,&apos;政治&apos;,&apos;技术&apos;]
def save_to_file(ans):
    filename = &amp;quot;[ANS] &amp;quot; + fn
    f = open(filename,&amp;quot;w&amp;quot;)
    f.writelines(ans)
    f.close()
    print(&amp;quot;Finished creating ANSWER!&amp;quot;)
	
def process(filename):
    n = 0
    file = open (filename,&amp;quot;r&amp;quot;)
    for eachline in file.readlines():
        if eachline[:22] == &apos;      &amp;quot;answerContent&amp;quot;:&apos;:
            n = n + 1
            mystr = eachline[23:]
            #mylist = eachline.split(&apos;:&apos;)
            &apos;&apos;&apos;			
            if &amp;quot;image/png;base64,&amp;quot;  in mystr:
            		mystr = &amp;quot;data:&amp;quot; + mystr
            &apos;&apos;&apos;
            if n &amp;lt;= 9:
                everyans = &amp;quot;0&amp;quot; + str(n) + &amp;quot;.  &amp;quot; + mystr
            else:
                everyans = str(n) + &amp;quot;.  &amp;quot; + mystr

            global ans
            ans.append(everyans)

        if eachline[:31] == &apos;  &amp;quot;questionPoolContentInfos&amp;quot;: [&apos;:
            save_to_file(ans)
            break

if __name__ == &amp;quot;__main__&amp;quot;:
    dataurl = input(&amp;quot;Please input downloadURL: &amp;quot;)
    os.system(&amp;quot;wget &amp;quot; + dataurl)
    list1 = dataurl.split(&apos;/&apos;)
    global fn
    fn = list1[-1].strip()
    print(&amp;quot;1.语文 2.数学 3.英语 4.物理 5.政治 6.技术&amp;quot;)
    global subject
    subject = int(input(&amp;quot;Please input subjectnum: &amp;quot;))
    fn = &amp;quot;[&amp;quot; + subjectlist[subject-1] + &amp;quot;] &amp;quot;+ fn[:4] + &amp;quot;...&amp;quot; + fn[-8:]
    print(&amp;quot;---------&amp;quot; + fn + &amp;quot;---------&amp;quot;)
    os.rename(list1[-1].strip(),fn)
    process(fn)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此脚本会下载答案文件，并自动提取答案&lt;/p&gt;
&lt;h3&gt;使用方法&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;输入答案下载链接（形如&lt;code&gt;http://xhfs2.oss-cn-hangzhou.aliyuncs.com/${ADRESS}.txt&lt;/code&gt;）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;下载完毕后，输入答案相应的学科编号，答案即刻生成。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.j10ccc.xyz/static/blog/8usO2R.png&quot; alt=&quot;8usO2R.png&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;$ ls
&apos;[ANS] [政治] 1234...abcd.txt&apos;  &apos;[政治] 1234...abcd.txt&apos;
# [政治] 1234...abcd.txt 是答案源文件
# [ANS] [政治] 1234...abcd.txt 是自动生成的答案
$ cat &apos;[ANS] [政治] 1234...abcd.txt&apos;
01.  &amp;quot;A&amp;quot;,
02.  &amp;quot;D&amp;quot;,
03.  &amp;quot;D&amp;quot;,
04.  &amp;quot;A&amp;quot;,
05.  &amp;quot;D&amp;quot;,
06.  &amp;quot;D&amp;quot;,
07.  &amp;quot;B&amp;quot;,
08.  &amp;quot;D&amp;quot;,
### 省略若干行，不止能生成选择题！

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;结尾&lt;/h2&gt;
&lt;p&gt;我得赶紧写作业去了。&lt;s&gt;毕竟开学考是逃不过的。。&lt;/s&gt;&lt;/p&gt;
</content:encoded></item><item><title>为什么有那么多意思相近却长相不同的单词</title><link>https://site.j10ccc.xyz/zh-cn/blog/history-of-english-zh-cn/</link><guid isPermaLink="true">https://site.j10ccc.xyz/zh-cn/blog/history-of-english-zh-cn/</guid><pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;背景&lt;/h2&gt;
&lt;p&gt;刚刚结束新学期的第一节英语课，课堂节奏紧凑，氛围却比较和谐，总的来说感觉以后会学到好多东西，毕竟这个老师非常🐂 第一节课介绍了冬奥会的一些内容，第二节课就开始讲英语的历史，感触非常深刻，于是记录了一些闻所未闻的知识。&lt;/p&gt;
&lt;h2&gt;引子&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;打个比方，中文里面的动物“猪”，变成食物“肉”，只需要加一个“肉”字，而英语单词中的猪 &lt;strong&gt;pig&lt;/strong&gt; ，变成肉 &lt;strong&gt;pork&lt;/strong&gt; ，有没有想过为什么会这样子？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;三次入侵对英语的影响&lt;/h2&gt;
&lt;p&gt;史上有三次入侵：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;拉丁语的传入&lt;/li&gt;
&lt;li&gt;北欧入侵&lt;/li&gt;
&lt;li&gt;诺曼征服&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;其中诺曼入侵影响最大，是法语在英语中扎根的开端。直到今天，日常英语中的法语单词仍然占大多数。&lt;/p&gt;
&lt;h2&gt;原生英语单词和法语单词的对比&lt;/h2&gt;
&lt;h3&gt;Food&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;English&lt;/th&gt;
&lt;th&gt;French&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;pig&lt;/td&gt;
&lt;td&gt;pork&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ox&lt;/td&gt;
&lt;td&gt;beef&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;calf&lt;/td&gt;
&lt;td&gt;veal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sheep&lt;/td&gt;
&lt;td&gt;mutton&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;deer&lt;/td&gt;
&lt;td&gt;venison&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Occupation&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;English -er&lt;/th&gt;
&lt;th&gt;French -or&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;baker&lt;/td&gt;
&lt;td&gt;sculptor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;shoemaker&lt;/td&gt;
&lt;td&gt;doctor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;builder&lt;/td&gt;
&lt;td&gt;tutor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;carpenter&lt;/td&gt;
&lt;td&gt;auditor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;painter&lt;/td&gt;
&lt;td&gt;tailor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;barber&lt;/td&gt;
&lt;td&gt;governer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;还有来自法语的一些奇奇怪怪的词根例如&lt;code&gt;eon&lt;/code&gt;，相应的单词如：&lt;code&gt;pigeno&lt;/code&gt;，&lt;code&gt;surgeon&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;分析&lt;/h2&gt;
&lt;p&gt;回到关于猪肉的争议。法国入侵之后，法语应该是社会的上层阶级使用的，而底层人民还是讲原始的英语为主。&lt;/p&gt;
&lt;p&gt;法语中的猪肉是&lt;code&gt;pork&lt;/code&gt;，英语中的猪是&lt;code&gt;pig&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;讲英语的底层人民只养猪，所以只说&lt;code&gt;pig&lt;/code&gt;，那么&lt;strong&gt;猪&lt;/strong&gt;就是&lt;code&gt;pig&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;高官贵人负责吃肉，所以&lt;strong&gt;猪肉&lt;/strong&gt;是&lt;code&gt;pork&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;长久的这样下来，就出现了&lt;strong&gt;猪&lt;/strong&gt;和&lt;strong&gt;猪肉&lt;/strong&gt;单词搭不上边的状况，类似的一些现象都是入侵后社会分层严重导致的&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;现在要学的好多意思非常相近的英文单词，基本上都是有一个是来自法语的。那么法语单词的入侵无疑增加了学习英语的难度（&lt;/p&gt;
</content:encoded></item><item><title>不可变的 JS 字符串，操作起来有这么暴力吗</title><link>https://site.j10ccc.xyz/zh-cn/blog/how-does-v8-handle-string-zh-cn/</link><guid isPermaLink="true">https://site.j10ccc.xyz/zh-cn/blog/how-does-v8-handle-string-zh-cn/</guid><pubDate>Fri, 12 Apr 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;你以为 JS 中的字符串是不可变的，对于 JS 语言的使用者来说确实是这样，每次对字符串操作后，总是会创建一个新的字符串，占用新的内存空间。但是 v8 在底层实现中，对字符串的操作做了优化，字符串的概念发生了一些微妙的变化。&lt;/p&gt;
&lt;h2&gt;拼接操作&lt;/h2&gt;
&lt;p&gt;我们先从拼接两个字符串入手，逐步了解 v8 对字符串操作的优化方向，以及分析这种黑箱优化对开发的性能影响。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const a = &amp;quot;a&amp;quot;;
const b = &amp;quot;b&amp;quot;;
const ab = a + b;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对我们开发者来说，&lt;code&gt;ab&lt;/code&gt; 是一个全新的字符串。然而当 &lt;code&gt;a&lt;/code&gt; 和 &lt;code&gt;b&lt;/code&gt; 都特别长的时候，如果直接暴力地给这个新字符串分配内存，会有很大的开销（算上原来的两个子字符串，总共占用了双倍的内存），V8 就对这个场景做了优化。参考 &lt;a href=&quot;https://github.com/v8/v8/blob/6813d83b76b725aaaeace8d377c2b602c6fd3c19/src/objects/string.h#L915-L923&quot;&gt;&lt;code&gt;v8/src/objects&lt;/code&gt; 中对 &lt;code&gt;ConString&lt;/code&gt; 的注释&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The ConsString class describes string values built by using the addition operator on strings. A ConsString is a pair where the first and second components are &lt;strong&gt;pointers to other string values&lt;/strong&gt;. One or both components of a ConsString can be pointers to other ConsStrings, &lt;strong&gt;creating a binary tree&lt;/strong&gt; of ConsStrings where the leaves are non-ConsString string values.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;v8 不管字符串长不长，只要是用 &lt;code&gt;+&lt;/code&gt; 运算符拼接的，都用 &lt;code&gt;ConString&lt;/code&gt; 来表示新字符串。同时 &lt;code&gt;ConString&lt;/code&gt; 不是一个新的字符串，他记录了参与运算的两个字符串的指针。&lt;code&gt;ConString&lt;/code&gt; 也可以由小的 &lt;code&gt;ConString&lt;/code&gt; 和非 &lt;code&gt;Constring&lt;/code&gt; 组合而成，最终的结构是一棵二叉树。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;再回过头来看，&lt;code&gt;a&lt;/code&gt; 和 &lt;code&gt;b&lt;/code&gt; 是最普通的字符串，他们不是 &lt;code&gt;ConString&lt;/code&gt;，而 &lt;code&gt;ab&lt;/code&gt; 是。这样字符串就出现了两个不同的种类（在底层的结构也不同）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这样是不是省下了不必要的内存开销？当然，拼接字符串也可以通过模板字符串来做，但是 v8 没有对这样的操作做类似的优化。可能未来会有。&lt;/p&gt;
&lt;h2&gt;切片操作&lt;/h2&gt;
&lt;p&gt;加法吃到的红利后，减法也想要引用原字符串来优化内存。现在讨论 &lt;code&gt;slice&lt;/code&gt; 方法，如果也用引用来处理的话，我们可以保存原字符串，和切片的两端索引。访问子串的时候，只需要给出原字符串在切片范围内的内容就行了。v8 源码中关于 &lt;code&gt;SlicedString&lt;/code&gt; 的解释在&lt;a href=&quot;https://github.com/v8/v8/blob/6813d83b76b725aaaeace8d377c2b602c6fd3c19/src/objects/string.h#L1023-L1033&quot;&gt;这里&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A Sliced String is described as a pointer to the parent,
the offset from the start of the parent string and the length.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;看上去好像不错？实际上当我们对一个很长的字符串切片的时候，得到的是一个很小的字符串，我们保存了结果的引用。但是这个小字符串在底层还引用着大字符串，这样 GC 就无法正常回收占用大内存的原字符串。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const longString = &amp;quot;#&amp;quot;.repeat(10_000);
const subString = longString.slice(50, 60);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个缺点在源码中也有提到&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Currently missing features are:&lt;/p&gt;
&lt;p&gt;- truncating sliced string to enable otherwise unneeded parent to be GC&apos;ed.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;到这里总结一下，如果底层的操作都基于“可变“的原则来编写，那么结果是有利有弊的，在缩小的操作中，我们需要直接创建一个新的字符串，而不是复用原字符串。&lt;/p&gt;
&lt;h2&gt;字符串操作的性能优化&lt;/h2&gt;
&lt;p&gt;我们对创建两种不同字符串的方法做个命名&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const a = &amp;quot;a&amp;quot;;
const b = &amp;quot;b&amp;quot;;

const NotConString = `${a}${b}`; // Mutation

const ConString = a + b; // Concatenation
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Concatenation 就是引用原字符串的方法，Mutation 则是创建一个完全新的字符串。我们希望在拼接的时候使用 Concatenation（当然 v8 默认就是这样实现，只要我们使用 &lt;code&gt;+&lt;/code&gt; 来拼接），在切片的时候使用 Mutation（当我们调用 String.slice() 的时候，默认是 Concatenation，我们需要额外处理这个）&lt;/p&gt;
&lt;p&gt;小字符串操作无所谓，怎么方便怎么来。在大字符串操作的场景下，对于拼接，尽量使用 &lt;code&gt;+&lt;/code&gt;，对于切片，使用了 &lt;code&gt;slice&lt;/code&gt; 方法之后，我们需要额外的释放掉原字符串的内存。可以使用其他的 Mutation 方法，比如说 &lt;code&gt;replace&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const longString = &amp;quot;#&amp;quot;.repeat(10_000);
let subString = longString.slice(50, 60);

// 替换掉一个完全不存在的子串
subString = subString.replace(&amp;quot;*&amp;quot;.repeat(subString.length + 1), &amp;quot;&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样可以强行复制切片后的子串内容，删除掉对原字符串的引用。&lt;/p&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;https://ysx.cosine.ren/optimizing-javascript-translate&lt;/li&gt;
&lt;li&gt;https://github.com/v8/v8/blob/main/src/objects/string.h&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>从函数注释看 TypeScript 和 JSDoc</title><link>https://site.j10ccc.xyz/zh-cn/blog/jsdoc-in-typed-function-zh-cn/</link><guid isPermaLink="true">https://site.j10ccc.xyz/zh-cn/blog/jsdoc-in-typed-function-zh-cn/</guid><pubDate>Wed, 27 Mar 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;前阵子 Svelte 宣布放弃 TypeScript，拥抱 JSDoc，这事整个社区都在讨论。不谈 TS 对构建的影响，当时聊的比较多的方向就是他们两个功能的交集 —— 类型。&lt;/p&gt;
&lt;p&gt;我从个人爱好看，更喜欢 TS 一点，论类型 JSDoc 的表现力肯定是不如 TS 的类型体操的。而且后者声明类型还比较麻烦。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;/**
 * @param a {number} variable a
 * @param b {number} variable b
 */
function sum(a, b) {
  return a + b;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;好，当时看完社区里面的文章我是这样想的，谁还用 JSDoc 啊？转头回去写代码，却自然而然写出这样子的&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;interface Person {
  /** 名字 */
  name: string;
  /** 年龄 */
  age: number;
  /** 身份 */
  role: string;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有啥不对的吗？我不自觉地在 TS 中使用了 JSDoc 来写注释， 编辑器则按照 JSDoc 的规范来解析我的注释，渲染到代码提示里面（hover 鼠标到属性上出现注释）&lt;/p&gt;
&lt;p&gt;我一直没意识到这样的注释就是 JSDoc，直到前几天有群友在问&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;TS 定义函数类型，要给参数列表写注释怎么写？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;接着他贴出了这样一段代码，又问到，你们都是写这么复杂的吗？&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;interface Fn {
  /**
   * @param {number} arg1 第一个参数
   */
  (arg1: number): void
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一开始我没看懂他的意思，后来尝试了一下用 &lt;code&gt;type&lt;/code&gt; 类型定义函数，是没办法给参数添加注释的。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;type Fn = (arg1: number) =&amp;gt; void;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是他的写法太抽象了，他居然 JSDoc 和 TS 混着用！&lt;s&gt;绝对是重大政治错误！&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;我尝试写他的 Demo 后才发现，他是想要复用这个注释，如果有变量的类型是这个 interface，那么自动会被编辑器解析出参数的 JSDoc。注释写在声明 interface 的地方，只写一次就行了。但是 type 做不到，要做到的话只能在声明函数的地方写一份 JSDoc。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.j10ccc.xyz/static/blog/vDEYmd.png&quot; alt=&quot;在函数定义的时候才写注释&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;此时我才意识到，TS 压根不带注释的功能，编辑器解析注释完全是靠 JSDoc 的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;再回看这个群友的问题，他认为写 interface 这样不够优雅（我也觉得不对），但是要在类型上&lt;strong&gt;复用注释&lt;/strong&gt;，就只能这样写。 既然 type 不支持写注释，那么问题就确定在该不该一起写 TS 和 JSDoc。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;有些开发者认为，变量名含义到位了就不需要注释，我认为编辑器对类型和注释两者的支持必须兼得，自己爱用啥用啥。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;后来找到个合适的答案, &lt;a href=&quot;https://www.reddit.com/r/typescript/comments/ya73vi/comment/itapxpg/?utm_source=share&amp;amp;utm_medium=web3x&amp;amp;utm_name=web3xcss&amp;amp;utm_term=1&amp;amp;utm_content=share_button&quot;&gt;Reuse TS @param comments&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;That’s not necessarily true. &lt;strong&gt;The type tells the what, but you may also want to give a description of the why for a param or a return value.&lt;/strong&gt; I agree that including the type in the jsdoc is unnecessary and is more prone to becoming outdated than the actual TS type, but jsdoc definitely still has a place alongside TS.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;加粗的文字解释得很清楚了，类型只是来定义一个东西是什么，如果想具体描述这个东西才用 JSDoc。&lt;strong&gt;声明类型的时候我只知道这个函数必要组成，声明函数的时候我才知道这个函数用来干啥&lt;/strong&gt;，类型只不过是函数的形状。也就是说，&lt;strong&gt;复用注释&lt;/strong&gt;这个想法在理论上是不对的。&lt;/p&gt;
&lt;p&gt;所以 JSDoc 应该写在声明函数的时候，而不是声明类型的时候。JSDoc 可以用来作为 TS 对注释不支持的补充。这样来看，他们两个是在定位上不同。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;/**
 * 
 * @param a variable a
 * @param b variable b
 */
const sum: Fn = (a, b) =&amp;gt; {
  return a + b;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意这里没有用 JSDoc 来声明类型，函数类型显式地被声明。JSDoc 和 TS 各司其职。&lt;/p&gt;
&lt;p&gt;回看 JSDoc 和 TS 的争议，他们两者仅仅是有类型这一交集。大部分 TS 开发完全可以用 JSDoc 来补充注释支持的短板，没有用了哪个就不能用哪个的说法。&lt;/p&gt;
</content:encoded></item><item><title>用NAP访问无公网IP的树莓派</title><link>https://site.j10ccc.xyz/zh-cn/blog/nap-proxy-zh-cn/</link><guid isPermaLink="true">https://site.j10ccc.xyz/zh-cn/blog/nap-proxy-zh-cn/</guid><pubDate>Mon, 03 Feb 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;你是否厌倦了puttty,Xshell的平庸外表，是否厌倦了在外地无法访问家中的树莓派，而运营商却死活不给固定IP，并且为树莓派买一个VPS大材小用而烦恼？本文提供了一种解决方案——使用较为经济的&lt;strong&gt;nap&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;准备&lt;/h2&gt;
&lt;h3&gt;安装ssh&lt;/h3&gt;
&lt;p&gt;Raspbian默认没有安装&lt;code&gt;openssh-service&lt;/code&gt;，先用&lt;code&gt;putty&lt;/code&gt;内网连接树莓派，使用&lt;code&gt;apt&lt;/code&gt;安装&lt;code&gt;openssh-service&lt;/code&gt;，完成后修改&lt;code&gt;sshd_config&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;$ sudo vim /etc/ssh/sshd_config
#修改： #PasswordAuthentication yes 为： PasswordAuthentication yes
#修改： PermitRootLogin prohibit-password 为： PermitRootLogin yes
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;注册下载&lt;/h3&gt;
&lt;p&gt;先照着Napyy的&lt;a href=&quot;https://napyy.com/blog/nap-tutorial/&quot; title=&quot;官方文档1&quot;&gt;官方文档&lt;/a&gt;注册，下载arm客户端到树莓派，解压压缩包（本文解压到了根目录&lt;code&gt;/home/pi/nap_linux_arm&lt;/code&gt;）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;#结构目录
pi@raspberrypi:~ $ tree
.
└── nap_linux_arm
    ├── nap
    ├── nap.ini
    ├── nohup.out
    └── start.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;尝试开启nap&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;$ ./nap
INFO[2020-01-19T21:44:48+08:00] login to server success
INFO[2020-01-19T21:44:48+08:00] proxy added: [tcp]
INFO[2020-01-19T21:44:48+08:00] start [tcp] proxy success
INFO[2020-01-19T21:44:48+08:00] forwarding napy.xyz:XXXXX -&amp;gt; 127.0.0.1:22
#如果出现上面这样的输出说明与主机连接成功
#下面检验是否成功穿透，当前终端让他运行着，打开另一个终端
$ ssh -oport={$REMOTE_PORT} pi@{$YOUR_HOST_ID}
#第一次连接会提示输入yes,接着输入密码就连接成功

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;配置启动脚本&lt;/h2&gt;
&lt;p&gt;在客户端目录下编写启动脚本&lt;code&gt;start.sh&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;#!/bin/sh
cd /home/pi/nap_linux_arm #nap_linux_arm为客户端文件夹名
nohup ./nap &amp;amp; #使用nohup命令，使nap在后台运行
# 使用nohup命令，会在同级目录下创建nohup.out来打印命令输出
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;给&lt;code&gt;start.sh&lt;/code&gt;添加权限&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;$ sudo chmod 777 start.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以先运行脚本，如果后台无nap进程，请查看&lt;code&gt;nohup.out&lt;/code&gt;分析错误原因。&lt;/p&gt;
&lt;p&gt;将脚本文件添加到启动项中&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;$ sudo vim /etc/rc.local
#找到&amp;quot;exit 0&amp;quot;的上一行，插入:
/home/pi/nap_linux_arm/start.sh start
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;结束&lt;/h2&gt;
&lt;p&gt;重启树莓派，等一会儿（开机和启动nap要时间），终端输入&lt;code&gt;ssh -oport={$REMOTE_PORT} pi@{$YOUR_HOST_ID}&lt;/code&gt;，根据提示输入密码即可成功连接，另外可以根据&lt;a href=&quot;https://napyy.com/blog/nap-custom-domains/&quot; title=&quot;官方文档2&quot;&gt;官方文档&lt;/a&gt;将&lt;code&gt;YOUR_HOST_ID&lt;/code&gt;自定义为你自己的域名。&lt;/p&gt;
&lt;h2&gt;评语&lt;/h2&gt;
&lt;p&gt;个人使用nap连接有点延迟，如果在树莓派上码字就很不舒服（应该没人在这上面写代码）,别问我为什么要用nap，问就是包不起VPS。。&lt;/p&gt;
</content:encoded></item><item><title>博客站点翻新了</title><link>https://site.j10ccc.xyz/zh-cn/blog/new-blog-astro-zh-cn/</link><guid isPermaLink="true">https://site.j10ccc.xyz/zh-cn/blog/new-blog-astro-zh-cn/</guid><pubDate>Thu, 10 Aug 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这几个星期把博客翻新了。从原来的 Hugo + 第三方主题换成了 Astro + 全自定义主题。&lt;/p&gt;
&lt;p&gt;这套主题我命名为 &lt;strong&gt;Stone&lt;/strong&gt;，颜色选取参考了身边一些建筑的颜色。目前没有计划将主题抽离出来做定制化配置。&lt;/p&gt;
&lt;p&gt;Astro 官方文档有一篇完整的构建博客教程，按照教程一步步来构建，对有前端基础的应该不难。这也是我用 Astro 的理由之一。&lt;/p&gt;
&lt;h2&gt;为什么选择 Astro?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Astro 基于 Vite 构建、并且在 NPM 生态圈内，开发体验好。&lt;/li&gt;
&lt;li&gt;Astro 可编程性高，支持 TS、文件路由、静态路由生成、周边插件等。&lt;/li&gt;
&lt;li&gt;模板语言为 JSX，上手很快，看文档了解 Astro 的一些工作原理后，碰到的问题基本上能自己解决&lt;/li&gt;
&lt;li&gt;文档全面，社区活跃。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我选择的是 SSG 构建，当然 Astro 也支持 SSR。得益于静态路由映射，全站包括文章页，首页的性能都很好，Lighthouse 的 Performance 和 SEO 分数都能打满。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.j10ccc.xyz/static/blog/fFO5eS.png&quot; alt=&quot;lighthouse.png&quot;&gt;&lt;/p&gt;
&lt;h2&gt;提一嘴&lt;/h2&gt;
&lt;p&gt;博客的主题我换过好几次，Hexo 时期用过三个，Hugo 用过一个，这次手写全站样式，应该是最后一个了。&lt;/p&gt;
&lt;p&gt;现在有很多博客生成方案，有用 Nextjs 搭建 Notion 知识库站点的，Umijs 官方也有个博客搭建教程。很多方案的文章数据都从现有站点的 API 获取，或者托管在 CMS 上，当然 Astro 也可以做到这个。而我更偏向于在本地存放，不说数据安全的问题，起码数据迁移成本更低。&lt;/p&gt;
&lt;p&gt;很多人现在也不会去搭建博客。飞书文档、语雀等文档（知识库）应用都足够好用，比 Markdown 有更丰富的展现形式。搭建博客更多还是个人的意愿，类似求职加分之类的理由。&lt;/p&gt;
&lt;p&gt;博客的初衷：分享知识，记录经历。在实现这两点之前有很多道坎。对很多人来说博客本身没什么意义，搭建的过程可能更有意义一些吧。&lt;/p&gt;
</content:encoded></item><item><title>一道调用堆栈的性能优化题</title><link>https://site.j10ccc.xyz/zh-cn/blog/optimize-stack-call-zh-cn/</link><guid isPermaLink="true">https://site.j10ccc.xyz/zh-cn/blog/optimize-stack-call-zh-cn/</guid><pubDate>Fri, 04 Nov 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;昨天参加了一场性能优化的比赛，题目很简单，优化思路是优化堆栈调用，但是第一次写这种题目，没发挥出来😵&lt;/p&gt;
&lt;p&gt;赛后我想了一下，在我代码的基础上，大概有两种不同程度的优化方案，但是这篇文章写着写着，发现堆栈调用是一个很大的坑&lt;/p&gt;
&lt;p&gt;顺便提一下，ACE的耗时是我（第五）的一半&lt;/p&gt;
&lt;h2&gt;原题&lt;/h2&gt;
&lt;p&gt;有删改，但是题意不变&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;struct vec {
  int *data;
  int length;
};

void get_vec_element(vec *v, int index, int *data) {
  int value;
  value = v-&amp;gt;data[index];
  *data = value;
}

int get_vec_length(vec *v) { return v-&amp;gt;length; }

/*
  v-&amp;gt;data = {1, 2, 3, 4, 5}
  sum = 1 + 2 + 3 + 4 + 5 = 15
*/
void combine(vec *v, int *dest) {
  // code here
  int i;

  *dest = 0;
  for (i = 0; i &amp;lt; get_vec_length(v); i++) {
    int val;
    get_vec_element(v, i, &amp;amp;val);
    *dest = *dest + val;
  }
}

int main() {
  vec v;
  v.data = new int[5]{1, 2, 3, 4, 5};
  v.length = 5;
  int sum = 0;

  for (int i = 0; i &amp;lt; 100000000; i++) {
	// 放大差异
    sum = 0;
    combine(&amp;amp;v, &amp;amp;sum);
  }

  delete[] v.data;

  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;约定&lt;/h2&gt;
&lt;p&gt;我在原题的基础上，在for循环的上下写了定时器，测出for循环结束的耗时，从而比较性能&lt;/p&gt;
&lt;h3&gt;运行环境&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Compiler: clang++@13.1.6
Build Tool: cmake@3.24.3
CPU: Apple M2
OS: macOS 12.4 21F2081 arm64
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;分析&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;data&lt;/code&gt;是一个指针，指向堆区的一个数组&lt;/li&gt;
&lt;li&gt;&lt;code&gt;get_vec_length()&lt;/code&gt;间接访问&lt;strong&gt;一次&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;get_vec_element()&lt;/code&gt;这个函数开销太大了，获取数组一个元素，访问堆区，又通过指针访问栈区的内存，一共&lt;strong&gt;两次&lt;/strong&gt;间接访问&lt;/li&gt;
&lt;li&gt;&lt;code&gt;combine&lt;/code&gt;这个函数中还有一个累加语句，&lt;strong&gt;两次&lt;/strong&gt;间接访问&lt;/li&gt;
&lt;li&gt;算下来一次循环需要&lt;strong&gt;五次&lt;/strong&gt;间接访问（循环外的不计算在内）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;答案&lt;/h2&gt;
&lt;h3&gt;Case1&lt;/h3&gt;
&lt;p&gt;赛场的时候想出来的，当时看提升幅度挺大的就直接交了&lt;/p&gt;
&lt;p&gt;&lt;code&gt;for&lt;/code&gt;的&lt;strong&gt;每一次循环&lt;/strong&gt;都会计算终止条件，即调用&lt;code&gt;get_vec_length()&lt;/code&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;两次间接访问 + 一次函数调用开销&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 109ms
void combine(vec *v, int *dest) {
  int i;

  int sum = 0;
  for (i = 0; i &amp;lt; get_vec_length(v); i++) {
    sum += v-&amp;gt;data[i];
  }

  *dest = sum;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Case2&lt;/h3&gt;
&lt;p&gt;朋友的答案，优化了一个函数调用&lt;/p&gt;
&lt;p&gt;累加全在栈区操作，一次&lt;code&gt;for&lt;/code&gt;循环内访问一次堆区，最后一步把数值赋给堆区&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 78ms
void combine2(vec *v, int *dest) {
  int i;
  int len = get_vec_length(v);

  int sum = 0;
  for (i = 0; i &amp;lt; len; i++) {
    sum += v-&amp;gt;data[i];
  }

  *dest = sum;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Case3&lt;/h3&gt;
&lt;p&gt;累加都在堆区操作，效率最高，但是不讲武德，把原数组改了&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 69ms
void combine3_0(vec *v, int *dest) {
  int i;
  int len = get_vec_length(v);

  for (i = 1; i &amp;lt; len; i++) {
    v-&amp;gt;data[i] += v-&amp;gt;data[i - 1];
  }

  *dest = v-&amp;gt;data[i - 1];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面有一个很奇怪的例子，我把累加换了一种写法&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 80ms
void combine3_1(vec *v, int *dest) {
  int i;
  int len = get_vec_length(v);

  for (i = 1; i &amp;lt; len; i++) {
    v-&amp;gt;data[i] = v-&amp;gt;data[i] + v-&amp;gt;data[i - 1];
  }

  *dest = v-&amp;gt;data[i - 1];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以发现两种累加写法实际上的调用是不一样的，仅在此情况下（多访问了一次堆内存）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;sum += v-&amp;gt;data[i];
sum = sum + v-&amp;gt;data[i]; // equivalent

v-&amp;gt;data[i] += v-&amp;gt;data[i - 1]; // 69ms
v-&amp;gt;data[i] = v-&amp;gt;data[i] + v-&amp;gt;data[i - 1]; // 79ms
// time-consumed
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;大佬的写法没去问，现场是比 case2 要快，可能比 case3 要快，自己也写不出来，摆烂了&lt;/p&gt;
&lt;p&gt;研究过程头很大，&lt;code&gt;operator+=&lt;/code&gt;出现不等价的结果，我想了好久，没想出来为什么。这个深究也没什么意义，实际使用中产生的性能差异微乎其微。&lt;/p&gt;
&lt;h2&gt;分享&lt;/h2&gt;
&lt;p&gt;减少堆栈调用这个优化方案在递归的时候也能用上，递归不断保留作用域内的变量，可能导致栈内存溢出，尾递归就能很好地解决问题&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int f(int n) { 
  // 函数不回立即返回
  // 每次调用的 n 都存在栈中
  return (n == 1) ? 1 : n + f(n - 1); 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int f_tail(int n, int res = 0) {
	// 参数在结束函数返回的时候就被释放
	// 没有任何变量留在栈内
  return (n == 0) ? res : f_tail(n - 1, res + n);
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>夏末对于一篇推文的遐思</title><link>https://site.j10ccc.xyz/zh-cn/blog/post-illusion-zh-cn/</link><guid isPermaLink="true">https://site.j10ccc.xyz/zh-cn/blog/post-illusion-zh-cn/</guid><pubDate>Wed, 31 Aug 2022 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;以此文纪念一个衰老的项目和我的大一&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;这个项目&lt;/h2&gt;
&lt;p&gt;项目是上学期参加互联网+的一个项目。&lt;/p&gt;
&lt;p&gt;文章里面提到的产品趋同，难发现创新点，最后发生角斗。这个现象非常真实了，参加项目类竞赛常常会碰到这种情况，一些团队的解决方案就是避开热点抢占新商机，然而避免不了做出一个和市场上已有的产品类似的试验品。那个项目就是这样，我发现契机，检索商业报告，整理思路，团队讨论，开始动手bp和代码之后，我才发现，市场上有的是这样的产品了。&lt;/p&gt;
&lt;p&gt;很不是滋味吧，别人早就走过和我们一样的路了。那个项目瞄准的是基层医疗，可以说是一个新型产业试验田。商机一直都在，当下大家做的都不怎么好的情况下，我想到的是缝合。bp照着现存痛点天马行空解决方案，项目特点和竞争对手的缺点反着来，未来规划随便写点，以新项目需要成长期来收尾，回想起来整个项目就像踩在一个虚无的台阶上鄙视众生。&lt;/p&gt;
&lt;p&gt;“我寻思你不是一个ppt比赛吗，那我们就按照可能的方向画好大饼。”我一直以这种思维在做这个项目，尽管我作为一个开发非常讨厌它。以前我一直认为脱离了技术的需求就是扯淡，预期和实际不匹配很让人头疼。&lt;strong&gt;但是这个比赛就是要想象力&lt;/strong&gt;。于是我们一开始就决定好了技术要做到什么程度，编写bp也不管实现了啥，就照着行业报告的痛点一直钻，在策划书上把同行卷死。也许是一开始就知道了技术实现不了，才会有这么大的胆子画饼，这是我意料之外的状况了。在有获奖经验的团队其实能发现，同样是优先考虑策划书，他们会把技术掐的很死，就用某些技术干这个活了，这样不兜大圈子，哪怕专业性很强，也能找到相关资料把内容填充好，省下一大笔功夫。&lt;/p&gt;
&lt;p&gt;从写完前端到修改队友产出的bp，从和指导老师交涉到处理项目提交材料，第一次做这种项目的负责人压力太大了。记得那天是我至今为止唯一一次码字码到手酸痛。实际上精神内耗也很重，项目负责人要关注比赛信息，组织队内的工作安排，收集整理资料和观点。&lt;/p&gt;
&lt;h2&gt;反思暑假&lt;/h2&gt;
&lt;p&gt;问我是几月份做的这个项目，我一时半会儿也答不上来，最后会支支吾吾告诉你大概是在5月。这段时间不长的，但是经历过那些度日如年的日子就会有后遗的幻觉。在得知校内一轮筛选失败后的那天晚上，我在部门办公室没睡着。当然我不服输呀，在队友、老师面前我宣告项目会重启，但是这个暑假过后，我有点动摇了。&lt;/p&gt;
&lt;p&gt;回想起整个大一，我没做出什么让我个人满意的项目，包括这个夏天，我在做的一直都是提升能力，是代码能力，而不是产品能力。抱歉，能做到的只有这么多了。&lt;/p&gt;
&lt;p&gt;这个夏天像是开了倍速，舞台上预埋的事件被加速播放。任何一个事件都转瞬即逝，而我并不想要停下来品味。我其实和文章作者挺像，爱知识涉猎，但那是以前的我。从刚进大学到现在，我能明显感觉到我的视野和思维被限制住了。大学里的所有事情一环套一环，每件事情好像都可以排在 TODOLIST 的第一位——和高中相比，静心思考的时间减少了。&lt;/p&gt;
&lt;h2&gt;未来&lt;/h2&gt;
&lt;p&gt;Xmind 团队做过一个实验，完全按照用户需求来重新设计一款思维导图App，结果做出来的是个类 Keynote。从一个角度不难发现：用户的需求是不是被市场上的一些产品影响到了？&lt;/p&gt;
&lt;p&gt;不谈那些已存在的优劣，好像所有的创意，&lt;strong&gt;在现实中都能找到已有的存在&lt;/strong&gt;：实体，或者仅仅是大家趋同的想法。比赛要求的创新确实是很难，凭口号喊出的创新也不是我要的（尽管有好的想法已经足够优秀，但是没能力去实现）&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;SummersDay 在我脑中诞生的那一刻，我去操场上一口气跑了10圈&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;4月份我发过一条朋友圈，说要做 Team Leader，还创建了 SummersDay。看_极简团队管理手册_一次次看到睡着，但是始终没有实践。我为 SummersDay 贡献过，但是进度还是一个原地踏步的状态，直到现在完全停滞。很羡慕文章作者能在&lt;strong&gt;纯粹&lt;/strong&gt;的渴望下实现价值，我静不下心来了，有很多想法出现，拖延，沉默，消失。更何况那些夸大其词的竞赛创意，我不适合去统筹着写那些策划书。&lt;/p&gt;
&lt;p&gt;没有任务的驱动，对我来说意味着更少的产出，以后编程相关大方向只会往就业发展了，没有那么多理想的事情可以真正去做，去实现，哪怕是对自己承诺过要完成的～&lt;/p&gt;
&lt;p&gt;说起来和刚上大学那会儿比起来，我现在也是个大忙人了（拍肚皮～）。没有准备浑浑噩噩对待接下来的三年，不辜负自己的努力，身边人的期待，事情一件件做下去，假期总会来到。&lt;/p&gt;
</content:encoded></item><item><title>给文章页添加 vim 键位的滚动快捷键</title><link>https://site.j10ccc.xyz/zh-cn/blog/post-view-vim-mode-zh-cn/</link><guid isPermaLink="true">https://site.j10ccc.xyz/zh-cn/blog/post-view-vim-mode-zh-cn/</guid><pubDate>Mon, 20 May 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;了解到知乎的回答页支持类似 vim 的按键操作之后，马上给博客复刻了一个。 这次的类 vim 按键机制的实现，不仅仅是只支持声明出来的特定按键，而是实现了一套状态机制，来兼容 vim 的按键逻辑。&lt;/p&gt;
&lt;p&gt;文章从最简单的单按键逻辑开始，再到考虑按键的组合，一步步构造出类似 vim 的组合按键的管理体系。&lt;/p&gt;
&lt;h2&gt;单按键场景&lt;/h2&gt;
&lt;p&gt;编辑器里面最常用的场景就是控制光标上下行移动，在 vim 里面是用 &lt;code&gt;j&lt;/code&gt; 和 &lt;code&gt;k&lt;/code&gt; 来控制的。那么在我们博客的文章页中，我希望通过这两个按键来实现页面的上下滚动。不难写出这样的代码&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;let isScrolling = false; // 滚动状态标志
const scrollAmount = 12; // 每次滚动的像素值
const scrollInterval = 16;
let scrollTimer: number; // 定时器

// 按下按键开始滚动，支持长按
document.addEventListener(&amp;quot;keydown&amp;quot;, function(event) {
  if (event.key === &amp;quot;j&amp;quot; || event.key === &amp;quot;k&amp;quot;) {
    if (!isScrolling) {
      isScrolling = true;
      scrollTimer = setInterval(() =&amp;gt; {
        window.scrollBy(0, scrollAmount * (event.key === &amp;quot;j&amp;quot; ? 1 : -1));
      }, scrollInterval);
    }
  }
});

// 松掉按键
document.addEventListener(&apos;keyup&apos;, function(event) {
  if ((event.key === &amp;quot;j&amp;quot; || event.key === &amp;quot;k&amp;quot;)&amp;amp;&amp;amp; isScrolling) {
    clearInterval(scrollTimer);
    isScrolling = false;
  }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样子确实是实现了逻辑，但是如果要支持更多的按键，我们要声明更多的监听器？如果只声明一个监听器，那么 callback 中对每种按键都有一个特殊的判断，会让代码不可维护。更别说在这个控制滚动的例子中，为了支持长按，&lt;code&gt;keydown&lt;/code&gt; 和 &lt;code&gt;keyup&lt;/code&gt; 两个回调函数中的逻辑是耦合的。&lt;/p&gt;
&lt;p&gt;我们得重新考虑，设计一套按键触发回调的系统。我第一个想到的是可以考虑用发布订阅模式来实现，这样能实现回调函数的逻辑解耦。但是我们的目标不仅于此，考虑到 vim 中还有按键组合触发的操作（顺序地按下多个按键之后才触发回调），如 &lt;code&gt;gg&lt;/code&gt; 回到文件顶部，我们还得设计一套机制来控制 &lt;strong&gt;按下了一个按键之后，是要触发回调，还是再等下一个按键按下后再执行回调&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;多按键命令式场景&lt;/h2&gt;
&lt;p&gt;以顺序按下 &lt;code&gt;gg&lt;/code&gt; 回到页面顶部为例，我们开始进入的核心设计部分。&lt;/p&gt;
&lt;p&gt;先提供一个操作顺序，来明确我们要实现的效果&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;按下 &lt;code&gt;j&lt;/code&gt; 页面向下滚动一段距离&lt;/li&gt;
&lt;li&gt;按下 &lt;code&gt;g&lt;/code&gt; 无事发生&lt;/li&gt;
&lt;li&gt;再按下 &lt;code&gt;g&lt;/code&gt; 页面滚动到顶部&lt;/li&gt;
&lt;li&gt;按下 &lt;code&gt;j&lt;/code&gt; 页面向下滚动一段距离&lt;/li&gt;
&lt;li&gt;按下 &lt;code&gt;gjg&lt;/code&gt; 无事发生&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;逻辑不复杂，我们在消费侧声明我要支持的按键（组合），以及触发之后的回调，这些按键组合和回调在我们的控制系统中保存着。消费侧大概像这样子使用&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const keyHandler = new VimModeKeyHandler();

keyHandler.subscribe(&amp;quot;gg&amp;quot;, handleLeapToTop);

// 加一个 hook 来支持长按结束后的回调
keyHandler.subscribe(&amp;quot;j&amp;quot;, handleScrollDown, {
  onKeyUp: handleStopScroll
});

keyHandler.start(); // 开始监听
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;控制系统维护一个栈，表示历史上&lt;strong&gt;按下过，但是没有被触发&lt;/strong&gt;的按键。每当控制系统监听到有按键按下的时候，把栈里面的所有按键和新按键拼接起来，看看有没有匹配到业务方声明的按键组合。匹配的逻辑如下&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;匹配到前缀（例子中我们声明了 &lt;code&gt;gg&lt;/code&gt; 两个按键连续按下才触发回调，匹配前缀的意思是我们监听到了第一个 &lt;code&gt;g&lt;/code&gt; 被按下），我们就把新按键入栈，不触发回调，继续等待下一个按键按下。&lt;/li&gt;
&lt;li&gt;完全匹配（举个例子，如果有声明 &lt;code&gt;gg&lt;/code&gt; 和 &lt;code&gt;gga&lt;/code&gt;，那么按下 &lt;code&gt;gg&lt;/code&gt; 后会匹配到前者，后者会被忽略）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;发布订阅的基础逻辑&lt;/h2&gt;
&lt;p&gt;初步先实现最简单的发布订阅。不考虑 vim 命令机制的细节，只做监听事件的挂载和销毁，以及兼容消费侧的调用方式。&lt;/p&gt;
&lt;p&gt;这里定义订阅者的数据结构，还有事件处理函数&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;interface Listener {
  /** 命令 */
  cmd: string;
  /** 回调函数 */
  callback: string;
}

// 事件处理函数支持两种事件就够了
interface EventHandler {
  keydown: (e: KeyboardEvent) =&amp;gt; void;
  keyup: (e: KeyboardEvent) =&amp;gt; void;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来实现发布订阅模式的整体。设计上是执行了 &lt;code&gt;start()&lt;/code&gt; 方法之后，才会开始监听键盘事件。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;export default class VimModeKeyHandler {
  private listeners: Array&amp;lt;Listeners&amp;gt;;
  private eventHandler: EventHandler;

  constructor() {
    this.listeners = [];
    this.eventHandler = {
      // 注意这里箭头函数内部的 this 指向
      keydown: () =&amp;gt; { /** TODO */ },
      keyup: () =&amp;gt; { /** TODO */ },
    };
  }

  public start() {
    document.addEventListener(&amp;quot;keydown&amp;quot;, this.eventHandler.keydown);
    document.addEventListener(&amp;quot;keyup&amp;quot;, this.eventHandler.keyup);
  }

  public destroy() {
    document.removeEventListener(&amp;quot;keydown&amp;quot;, this.eventHandler.keydown);
    document.removeEventListener(&amp;quot;keyup&amp;quot;, this.eventHandler.keyup);
  }

  public subscribe(
    cmd: string,
    callback: () =&amp;gt; void,
    options: {
      onKeyUp?: () =&amp;gt; void
    } = {},
  ) {
    this.listeners.push({ cmd, callback, ...options });
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里单独把 &lt;code&gt;eventHandler&lt;/code&gt; 声明出来，保存回调函数，而不是直接把回调函数写在 &lt;code&gt;addEventListener&lt;/code&gt; 中，是为了方便后续销毁事件。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;eventHandler&lt;/code&gt; 的实现中，还有一点实现要注意。就是事件回调函数内部的 &lt;code&gt;this&lt;/code&gt; 指向。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;this.eventHandler = {
  [someEventName]: () =&amp;gt; { /** TODO */ },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;eventHandler&lt;/code&gt; 的每个事件回调函数中，我们预期会使用 class 中的一些属性，比方说我们保存每个历史按键的栈（尽管现在还没实现）。在 &lt;code&gt;start&lt;/code&gt; 函数中，&lt;code&gt;eventHandler.[eventCallback]&lt;/code&gt; 会被作为回调函数，传给 &lt;code&gt;addEventListener&lt;/code&gt;。如果回调函数的 &lt;code&gt;this&lt;/code&gt; 没确定下来，那么将无法访问类中的变量了。所以这里用了剪头函数来声明，函数中的 &lt;code&gt;this&lt;/code&gt; 指向类本身。&lt;/p&gt;
&lt;p&gt;当然你会想到用 function 来声明回调函数，然后 &lt;code&gt;addEventListener&lt;/code&gt; 的时候用 &lt;code&gt;bind&lt;/code&gt; 来修改 &lt;code&gt;this&lt;/code&gt; 指向。但是别忘了，这样会生成一个新的函数作用域的函数，会给销毁函数 &lt;code&gt;destory&lt;/code&gt; 的实现增加难度。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// 不推荐的写法
class VimModeKeyHandler {
  constructor() {
    this.eventHandler = {
      keydown: function () { /** TODO */ },
    };
  }

  public start() {
    // 创建了一个新的函数
    document.addEventListener(&amp;quot;keydown&amp;quot;, this.eventHandler.keydown.bind(this));
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;code&gt;keydown&lt;/code&gt; 中的逻辑&lt;/h2&gt;
&lt;p&gt;这部分是机制的核心实现。这部分逻辑概括着来说就是，实现历史按键序列，和已声明的命令匹配的逻辑。&lt;/p&gt;
&lt;p&gt;先给类加上个 &lt;code&gt;stack&lt;/code&gt; 属性，用来保存历史按键记录。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;class VimModeKeyHandler {
  // ...
  private stack: string[];

  constructor() {
    // ...
    this.stack = [];
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实现匹配逻辑，有三种匹配结果。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;匹配状态&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;th&gt;触发逻辑&amp;lt;br&amp;gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;完全匹配&lt;/td&gt;
&lt;td&gt;有条命令和输入序列一模一样。&lt;/td&gt;
&lt;td&gt;执行订阅者的回调函数，然后退出寻找匹配的循环。清空历史输入序列。&amp;lt;br&amp;gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;前缀匹配&lt;/td&gt;
&lt;td&gt;当前历史输入序列是某个命令的前缀。&lt;/td&gt;
&lt;td&gt;设置个标记，标记存在这样的命令，继续循环，尝试寻找完全匹配。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;匹配失败&lt;/td&gt;
&lt;td&gt;表示历史输入序列是非法的，连一个前缀匹配都没有。&lt;/td&gt;
&lt;td&gt;清空历史输入序列。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;this.eventHandler = {
  keydown: (e) =&amp;gt; {
    this.stack.push(e.key);
    const toMatch = this.stack.join(&amp;quot;&amp;quot;);

    let hasMatchedPrefix = false;
    for (const listener of this.listeners) {
      if (listener.cmd.startsWith(toMatch)) {
        hasMatchedPrefix = true;
        if (listener.cmd === toMatch) {
          listener.callback();
          this.stack = [];
          break;
        }
      }
    }
    if (!hasMatchedPrefix) this.stack = [];
  },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在录入按键的时候，我们是把按键作为文本放进历史输入序列中。这里还可以做些操作来兼容组合键的输入，例如区分大写字母 A &lt;code&gt;shift+a&lt;/code&gt;，或者其他的组合键。你可以把他们标记上特殊的符号，同时修改匹配函数，这样就能兼容更多样的输入组合。&lt;/p&gt;
&lt;p&gt;到这里我们实现了最基本的 &lt;code&gt;keydown&lt;/code&gt; 功能。&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;keyup&lt;/code&gt; 中的逻辑&lt;/h2&gt;
&lt;p&gt;需要用到 &lt;code&gt;keyup&lt;/code&gt; 的情况很少，上面提到了要兼容按键长按。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我猜测 vim 中应该是没有处理长按的逻辑的。长按应该也是被按照键入阈值来分割成多个按下事件。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我们可以粗暴地比较 &lt;code&gt;event.key&lt;/code&gt; 和 声明的命令，把 &lt;code&gt;keyup&lt;/code&gt; 事件给到订阅者，不再处理历史序列的匹配（因为长按是一次单键，或者是一次组合键，没有序列的概念了）。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;this.eventHandler = {
  keyup: (e) =&amp;gt; {
    for (const listener of this.listeners) {
      if (typeof listener.onKeyUp === &amp;quot;function&amp;quot; &amp;amp;&amp;amp; e.key === listener.cmd) {
        listener.onKeyUp(e);
      }
    }
  }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;function handleStopScroll() {
  if (isScrolling) {
    clearInterval(scrollTimer);
    isScrolling = false;
  }
}

keyHandler.subscribe(&amp;quot;j&amp;quot;, () =&amp;gt; handleScroll(&amp;quot;down&amp;quot;), {
  onKeyUp: handleStopScroll
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;到这里我们基本实现了 vim 的按键机制，我博客中的实现在这里&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/j10ccc/my-site/blob/main/src/scripts/vim-mode/key-handler.ts&quot;&gt;Github - @j10ccc/my-site/src/scripts/vim-mode/key-handler.ts&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>青训营 | 基于联想查词的实现</title><link>https://site.j10ccc.xyz/zh-cn/blog/qxy-punch-day1-zh-cn/</link><guid isPermaLink="true">https://site.j10ccc.xyz/zh-cn/blog/qxy-punch-day1-zh-cn/</guid><pubDate>Sun, 08 May 2022 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;我在掘金上的笔记搬运&lt;/p&gt;
&lt;p&gt;https://juejin.cn/post/7095178555968978952&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这是我参与「第三届青训营 -后端场」笔记创作活动的的第1篇笔记&lt;/p&gt;
&lt;p&gt;我是有 Java 基础的前端，这次被朋友拉下来学习后端，来了解下后端的程序设计，从而提高业务对接效率&lt;/p&gt;
&lt;p&gt;话不多说，先上手作业第二题目&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;一个利用现成 API 的词典程序&lt;/p&gt;
&lt;p&gt;可是作为第一个作品，我绝对不能做地这么简单！！！&lt;/p&gt;
&lt;p&gt;我要做一个基于联想的查词程序！！！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;需求分析&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;词典应用，首先使用场景是英译中&lt;/li&gt;
&lt;li&gt;&lt;em&gt;联想&lt;/em&gt;就是输入半个单词，列出所有你可能想输入的单词和其中文解释&lt;/li&gt;
&lt;li&gt;上网查了下，大部分翻译平台的接口都有 cookie 验证，这对我们长期的稳定功能实现很不友好，但是找不到啥好的平台了，就拿 &lt;strong&gt;有道词典&lt;/strong&gt; 凑合一下了&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;查看请求&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.j10ccc.xyz/static/blog/28f7e3.png&quot; alt=&quot;28f7e3.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.j10ccc.xyz/static/blog/1728ee.png&quot; alt=&quot;1728ee.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;复制了 curl 命令格式之后，转换 go 代码粘贴到文件一气呵成&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import (
	&amp;quot;fmt&amp;quot;
	&amp;quot;io/ioutil&amp;quot;
	&amp;quot;log&amp;quot;
	&amp;quot;net/http&amp;quot;
)

func main() {
	client := &amp;amp;http.Client{}
	req, err := http.NewRequest(&amp;quot;GET&amp;quot;, &amp;quot;https://dict.youdao.com/suggest?num=5&amp;amp;ver=3.0&amp;amp;doctype=json&amp;amp;cache=false&amp;amp;le=en&amp;amp;q=dict&amp;quot;, nil)
	if err != nil {
		log.Fatal(err)
	}
	req.Header.Set(&amp;quot;Accept&amp;quot;, &amp;quot;application/json, text/plain, */*&amp;quot;)
	req.Header.Set(&amp;quot;Accept-Language&amp;quot;, &amp;quot;zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6&amp;quot;)
	req.Header.Set(&amp;quot;Cache-Control&amp;quot;, &amp;quot;no-cache&amp;quot;)
	req.Header.Set(&amp;quot;Connection&amp;quot;, &amp;quot;keep-alive&amp;quot;)
	req.Header.Set(&amp;quot;Cookie&amp;quot;, `已打马`)
	req.Header.Set(&amp;quot;Origin&amp;quot;, &amp;quot;https://www.youdao.com&amp;quot;)
	req.Header.Set(&amp;quot;Pragma&amp;quot;, &amp;quot;no-cache&amp;quot;)
	req.Header.Set(&amp;quot;Referer&amp;quot;, &amp;quot;https://www.youdao.com/&amp;quot;)
	req.Header.Set(&amp;quot;Sec-Fetch-Dest&amp;quot;, &amp;quot;empty&amp;quot;)
	req.Header.Set(&amp;quot;Sec-Fetch-Mode&amp;quot;, &amp;quot;cors&amp;quot;)
	req.Header.Set(&amp;quot;Sec-Fetch-Site&amp;quot;, &amp;quot;same-site&amp;quot;)
	req.Header.Set(&amp;quot;User-Agent&amp;quot;, &amp;quot;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.54 Safari/537.36 Edg/101.0.1210.39&amp;quot;)
	req.Header.Set(&amp;quot;sec-ch-ua&amp;quot;, `&amp;quot; Not A;Brand&amp;quot;;v=&amp;quot;99&amp;quot;, &amp;quot;Chromium&amp;quot;;v=&amp;quot;101&amp;quot;, &amp;quot;Microsoft Edge&amp;quot;;v=&amp;quot;101&amp;quot;`)
	req.Header.Set(&amp;quot;sec-ch-ua-mobile&amp;quot;, &amp;quot;?0&amp;quot;)
	req.Header.Set(&amp;quot;sec-ch-ua-platform&amp;quot;, `&amp;quot;Windows&amp;quot;`)
	resp, err := client.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()
	bodyText, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf(&amp;quot;%s\n&amp;quot;, bodyText)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;请求头参数很多，自己写的话会麻烦，就使用在线平台转换了，&lt;s&gt;利用现成工具解决问题也是程序员要修炼的&lt;/s&gt;&lt;/p&gt;
&lt;h2&gt;分析处理请求&lt;/h2&gt;
&lt;p&gt;GET 请求 URL 中的参数就是我们要查的单词，所以得改成从运行命令的第二个参数读取&lt;/p&gt;
&lt;p&gt;在&lt;code&gt;main&lt;/code&gt;函数开头添加&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;if len(os.Args) != 2 {
    fmt.Fprintf(os.Stderr, `usage: simpleDict WORD example: simpleDict hello`)
    os.Exit(1)
}
word := os.Args[1] // word 就是我们要查询的单词
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再把 &lt;code&gt;word&lt;/code&gt; 拼接到请求 URL 中就能得到返回的 JSON 字符串了，现在我们要做的就是美化工作&lt;/p&gt;
&lt;h2&gt;分析返回数据&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.j10ccc.xyz/static/blog/6a40a2.png&quot; alt=&quot;6a40a2.png&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，联想词和翻译都已经有了&lt;/p&gt;
&lt;p&gt;&lt;code&gt;entries&lt;/code&gt;数组中的所有元素？拿来吧你！&lt;/p&gt;
&lt;p&gt;于是我飞快地写下&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;for _, item := range bodyText.entries {
    fmt.Printf(&amp;quot;%s&amp;quot;, item)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结果。。报错了&lt;/p&gt;
&lt;p&gt;我一看，发现&lt;code&gt;bodyText&lt;/code&gt;的类型是&lt;code&gt;byte&lt;/code&gt;数组，是一个扁平的结构，所以不能直接用 JS 的方式访问成员。&lt;/p&gt;
&lt;p&gt;这里扯一嘴，数组的每个元素是这串 JSON 的每个&lt;strong&gt;字节&lt;/strong&gt;，这意味着对于一个汉字需要多个元素来储存，测试了有限个数的汉字，大概是三个？&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;%E7%9F%A5%E4%B9%8E%E4%B8%80%E4%B8%AA%E6%B1%89%E5%AD%97%E5%8D%A0%E5%A4%9A%E5%B0%91%E5%AD%97%E8%8A%82?&quot;&gt;https://www.zhihu.com/question/20451870&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;格式化输出&lt;/h2&gt;
&lt;p&gt;要想访问就得把它反序列化才行，先去写个结构体&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;type DictReponse struct {
    Result struct {
        Msg string `json:&amp;quot;msg&amp;quot;`
        Code int `json:&amp;quot;code&amp;quot;`
    } `json:&amp;quot;result&amp;quot;`
    Data struct {
        Entries []struct {
            Explain string `json:&amp;quot;explain&amp;quot;`
            Entry string `json:&amp;quot;entry&amp;quot;`
        } `json:&amp;quot;entries&amp;quot;`
        Query string `json:&amp;quot;query&amp;quot;`
        Language string `json:&amp;quot;language&amp;quot;`
        Type string `json:&amp;quot;type&amp;quot;`
    } `json:&amp;quot;data&amp;quot;`
}

// some code here...

var dictResponse DictResponse
err = json.Unmarshal(bodyText, &amp;amp;dictResponse)
if err != nil {
    log.Fatal(err)
}

for _, item := range dictResponse.Data.Entries {
    fmt.Printf(&amp;quot;%s : %s\n&amp;quot;, item.Entry, item.Explain)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.j10ccc.xyz/static/blog/40f760.png&quot; alt=&quot;40f760.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;滴～ 提前下班&lt;/p&gt;
</content:encoded></item><item><title>青训营 | Git-进化-团队协作开发</title><link>https://site.j10ccc.xyz/zh-cn/blog/qxy-punch-day2-zh-cn/</link><guid isPermaLink="true">https://site.j10ccc.xyz/zh-cn/blog/qxy-punch-day2-zh-cn/</guid><pubDate>Wed, 15 Jun 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这是我参与「第三届青训营 -后端场」笔记创作活动的的第2篇笔记&lt;/p&gt;
&lt;h1&gt;git 的作用&lt;/h1&gt;
&lt;p&gt;版本控制，简单的四个字却需要很多操作来实现它&lt;/p&gt;
&lt;p&gt;原理是你修改代码并上传的时候，git 会找出 (diff) 修改后代码相较于上一个版本的修改内容，你实际上是在提交代码的修改。&lt;/p&gt;
&lt;p&gt;通过记录每次修改，从而实现版本回退，版本合并等操作，同时能让每一次修改都有迹可循&lt;/p&gt;
&lt;h1&gt;git 经常出现的几个概念&lt;/h1&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;概念&lt;/th&gt;
&lt;th&gt;解释&lt;/th&gt;
&lt;th&gt;备注&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;仓库 repo&lt;/td&gt;
&lt;td&gt;一个文件夹，存放代码的地方&lt;/td&gt;
&lt;td&gt;有本地库和和远程库之分，一个本地库，可以关联到多个远程库&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;代码托管平台&lt;/td&gt;
&lt;td&gt;里面有千千万万个远程仓库&lt;/td&gt;
&lt;td&gt;例如 Github, Gitlab&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;远程 remote&lt;/td&gt;
&lt;td&gt;远程仓库的标识&lt;/td&gt;
&lt;td&gt;默认的远程名是&lt;code&gt;origin&lt;/code&gt;，可以随便取名，用于区分不同仓库&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;分支 branch&lt;/td&gt;
&lt;td&gt;文件夹的一个属性&lt;/td&gt;
&lt;td&gt;默认为&lt;code&gt;master&lt;/code&gt;，分支发生改变，文件夹中的文件也会改变&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;暂存区&lt;/td&gt;
&lt;td&gt;在上传的代码的整个过程中，会先存到这里&lt;/td&gt;
&lt;td&gt;这里的修改可以撤回，可以覆盖&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;提交 commit&lt;/td&gt;
&lt;td&gt;提交暂存区中的修改到本地库，并附上一小段话概括你干了啥&lt;/td&gt;
&lt;td&gt;提交了就记为一个新版本，不允许修改了嗷&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h1&gt;常见的命令&lt;/h1&gt;
&lt;ol start=&quot;0&quot;&gt;
&lt;li&gt;
&lt;p&gt;创建本地 git 环境
有两种方法，第一种是去下载已经存在的仓库的代码，第二种是先本地开个库然后绑定到远程库&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;克隆远程仓库&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git clone $ssh_link
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时的本地库会被创建，同时绑定到一个远程库，远程库名字默认为 &lt;code&gt;origin&lt;/code&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;ssh link 就是以 .git 结尾的那个链接，在此之前你需要在代码托管平台认证你本地上传代码的设备的 ssh key，也就是认证这台设备是属于你的账号的&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;不用纠结克隆特定分支要加什么参数，因为 clone 操作会获取远程库中所有的分支信息，之后再切换到想要的分支即可&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;初始化仓库 / 添加远程库&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git init # 将该文件夹变成一个本地仓库(添加了 .git 文件夹)
git remote add $remote_repo_name $repo_ssh_link
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;添加远程可以添加多个，名字随便取，但是后期将本地库代码上传到远程的时候，要以名字指定上传到哪个远程库&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;添加修改文件到暂存区&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 添加指定文件
git add ${filename}
# 添加所有修改过的文件（git知道哪些文件发生了修改）
git add -A
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看暂存区中的内容&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git status
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;提交暂存区所有内容到本地库&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git commit -m &amp;quot;$commit_info&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;commit 附加的信息是用自己的话来描述你此次提交修改干了啥，虽然是自己输入，但在团队开发中最好要有个&lt;a href=&quot;https://www.jianshu.com/p/201bd81e7dc9&quot;&gt;规范&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;commit 无结尾引号时，在结尾输入换行符会到下一行继续输入 commit，直到输入结尾引号再回车就能完成 commit&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.j10ccc.xyz/static/blog/f24027.png&quot; alt=&quot;f24027.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.j10ccc.xyz/static/blog/81a9af.png&quot; alt=&quot;81a9af.png&quot;&gt;&lt;/p&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;
&lt;p&gt;查看 / 添加 / 切换分支&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git branch
git checkout -b $new_branch_name
git checkout $existed_branch_name
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通常地，项目的主分支是 &lt;code&gt;master&lt;/code&gt;，同时也会添加一个开发分支&lt;code&gt;dev&lt;/code&gt;，以及一些&lt;code&gt;feat-something&lt;/code&gt;分支，一般的开发都会在在开发分支中提交修改，&lt;code&gt;feat&lt;/code&gt;分支用于不同成员开发不同的功能&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看当前分支下的所有 commit&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git log
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;合并分支
当一个功能基本上完善了之后，需要合并&lt;code&gt;fix&lt;/code&gt;提交并&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;进阶操作&lt;/h2&gt;
&lt;h3&gt;修改默认编辑器&lt;/h3&gt;
&lt;p&gt;在 Linux 上编辑器一般都是 nano，如果要改成 vim，则需要在&lt;code&gt;.git/config&lt;/code&gt;添加&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-config&quot;&gt;[core]
    editor=vim
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;将当前分支的 commit 推送到远程库&lt;/h3&gt;
&lt;p&gt;要推送到 origin 库的&lt;strong&gt;同名分支&lt;/strong&gt;下，push 后面不加任何参数，默认推送到 origin 库的同名分支下&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# now in branch dev
git push origin dev

git push
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;拉取远程库最新的提交&lt;/h3&gt;
&lt;p&gt;如果远程库的当前分支的提交记录被更新了，但是本地却还是旧的版本，可以使用 pull 命令来更新（不是同步，本地库独有的提交记录不会被覆盖）提交，注意如果本地库有未完成的提交，需要先完成提交再进行 pull 操作&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git pull
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;发起分支合并&lt;/h3&gt;
&lt;p&gt;在 feat 分支下完成了功能的开发可以将其合并到 master 分支&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# now in branch feat
git checkout master
git merge feat
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;修改提交信息&lt;/h3&gt;
&lt;p&gt;打开编辑器，修改最近一次提交信息&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git commit --amend
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;添加版本信息&lt;/h3&gt;
&lt;p&gt;前端项目可以结合 &lt;code&gt;child_process&lt;/code&gt; 在 runtime 自动获取 git 提交版本信息，渲染到页面上&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git tag $version_string
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;清理提交信息&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;git rebase&lt;/code&gt; 的作用是&lt;strong&gt;清理、整合&lt;/strong&gt; commit 列表，在合作开发中有很重要的作用&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这个操作非常危险，他能合并多条 commit 成一条，并且此操作时不可逆的，即无法查看每条 commit 到底修改了什么，各条 commit 信息都合在一起了，只能看到合并的这堆 commit 修改了什么。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;下面列举了两个常见的场景：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;开发到一半，有换设备同步开发的需求，需要临时上传代码到远程库，这个时候 commit 信息就可以随便写点，到开发完了将无用的临时 commit 合并，正经写一次提交信息&lt;/li&gt;
&lt;li&gt;一个 feature 的开发需要多次 commit，但是将这么多的 commit 合并到主分支时显得有点繁琐，在主分支下可以合并多次的提交信息为一条 commit，当然 feat 分支仍然保存着每次 commit 的具体内容&lt;/li&gt;
&lt;li&gt;将没用的 merge commit 隐藏掉&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.j10ccc.xyz/static/blog/jym56J.png&quot; alt=&quot;jym56J.png&quot;&gt;
如图所示，我想要将&lt;code&gt;d576d7b&lt;/code&gt; ~ &lt;code&gt;36f5e7e&lt;/code&gt;这几次 commit 合并成一条 commit，则需要选择&lt;code&gt;36f5e7e&lt;/code&gt;的前一条 commit &lt;code&gt;cb759ee&lt;/code&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意，这些 commit 按照提交时间顺序排序，选择的那条 commit 一定是这几条中提交时间最早的&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git rebase -i cb759ee
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后会打开 git 默认的编辑器，保持最早的 commit 前为&lt;code&gt;pick&lt;/code&gt;，其他的都是&lt;code&gt;s&lt;/code&gt;就行了，保存之后会引导编辑合并后的 commit 信息，将默认保留的提交信息注释掉，重新写即可&lt;/p&gt;
&lt;h1&gt;推荐&lt;/h1&gt;
&lt;p&gt;VSCode 上有 &lt;a href=&quot;https://github.com/mhutchie/vscode-git-graph&quot;&gt;Git graph&lt;/a&gt; 插件，可以直观的检查该项目的所有提交记录，分支信息，远程库，Tags等&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.j10ccc.xyz/static/blog/jyEg2j.png&quot; alt=&quot;jyEg2j.png&quot;&gt;&lt;/p&gt;
</content:encoded></item><item><title>青训营 | React 实现简单的搜索结果渲染</title><link>https://site.j10ccc.xyz/zh-cn/blog/qxy-punch-day3-zh-cn/</link><guid isPermaLink="true">https://site.j10ccc.xyz/zh-cn/blog/qxy-punch-day3-zh-cn/</guid><pubDate>Thu, 16 Jun 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这是我参与「第三届青训营 -后端场」笔记创作活动的的第3篇笔记&lt;/p&gt;
&lt;p&gt;搜索引擎项目有个需求就是渲染搜索结果条目，为了让条目更加美观，我参考了 Google 的设计，顶部为 url 渲染，主体是关键词和详细结果展示。&lt;/p&gt;
&lt;p&gt;使用 React 框架，组件库用到了 antd&lt;/p&gt;
&lt;h2&gt;结果展示&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Google 中的结果条目截图
&lt;img src=&quot;https://cdn.j10ccc.xyz/static/blog/3e4af0.png&quot; alt=&quot;3e4af0.png&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;我实现的结果条目截图
&lt;img src=&quot;https://cdn.j10ccc.xyz/static/blog/eb89f9.png&quot; alt=&quot;eb89f9.png&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;后端返回数据样例&lt;/h2&gt;
&lt;p&gt;后端的搜索数据是爬取的人民日报文章数据，其中Content是整篇文章的内容（返回的内容过长，有删改&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
    &amp;quot;ID&amp;quot;: 1729,
    &amp;quot;URL&amp;quot;: &amp;quot;http://paper.people.com.cn/rmrb/html/2021-10/06/nw.D110000renmrb_20211006_5-01.htm&amp;quot;,
    &amp;quot;Title&amp;quot;: &amp;quot;有了社区食堂，真暖心（奋斗百年路 启航新征程·同心奔小康）&amp;quot;,
    &amp;quot;Content&amp;quot;: &amp;quot;　　中午时分，年过八旬的郑州市金水区甲院社区居民张玉玲和几位老姐妹相继走进社区食堂。&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;需求分析&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;URL 根据层级切片，域名和路径分不同颜色渲染&lt;/li&gt;
&lt;li&gt;对于较长的 Content ，前端要提取摘要，该摘要需要包含关键词&lt;/li&gt;
&lt;li&gt;卡片中需要处理溢出内容，在结尾添加省略号&lt;/li&gt;
&lt;li&gt;关键词高亮&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;提取摘要&lt;/h2&gt;
&lt;p&gt;因为 Content 过长，如果直接存在状态中，后续处理数据将会影响性能，所以选择在接受请求的时候就做好预处理&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;保留关键字&lt;/li&gt;
&lt;li&gt;第一个关键字所在的句子的完整性&lt;/li&gt;
&lt;li&gt;在上面两条前提下尽量缩短长度&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;先找到第一个关键词，然后向前寻找第一个标点符号，从标点符号后面的内容即是要保留的内容。数了一下，页面显示两行摘要大概是 80 个字符，所以我们保留 100 个字符，这样就获得摘要啦&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const pos = item.content.indexOf(keyWord);
const reg = /[^A-Za-z0-9\u4e00-\u9fa5+]/g;
let i = pos;
for (i = pos; i &amp;gt; 0; i--) {
  if (reg.test(item.content[i])) break;
}
const content = item.content.slice(
  i + 1,
  i + 100 &amp;lt;= item.content.length ? pos + 100 : item.content.length
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;URL 渲染&lt;/h2&gt;
&lt;p&gt;首先要区分颜色，先分出 &lt;code&gt;baseURL&lt;/code&gt;，我使用正则&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const pattern: RegExp = /^(http:\/\/|https:\/\/)[^/]+\//;
const baseURL = pattern
  .exec(url)![0]
  .slice(0, pattern.exec(url)![0].length - 1);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后把后面的路径中的&lt;code&gt;/&lt;/code&gt;替换成其他符号，我这里选择了&lt;code&gt;&amp;gt;&lt;/code&gt;，当然也可以用 emoji 连接😁&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;let s: string = &amp;quot;&amp;quot;;
url
  .split(baseURL + &amp;quot;/&amp;quot;)[1]
  .split(&amp;quot;/&amp;quot;)
  .forEach((item: string) =&amp;gt; {
    s += &amp;quot; &amp;gt; &amp;quot; + item;
  });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后渲染出来&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// 对于链接过长溢出，我使用了 antd 的 Typography 来处理
return (
  &amp;lt;Text ellipsis className=&amp;quot;fit-width&amp;quot;&amp;gt;
    &amp;lt;a href={url} style={{ fontFamily: &amp;quot;monospace&amp;quot; }}&amp;gt;
      &amp;lt;span style={{ color: &amp;quot;black&amp;quot; }}&amp;gt;{baseURL}&amp;lt;/span&amp;gt;
      &amp;lt;span style={{ color: &amp;quot;gray&amp;quot; }}&amp;gt;{s}&amp;lt;/span&amp;gt;
    &amp;lt;/a&amp;gt;
  &amp;lt;/Text&amp;gt;
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;关键词高亮&lt;/h2&gt;
&lt;p&gt;对于较短的摘要和结果标题，先切片再拼接将内容和关键词独立出来&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;export default function RichText(props: any) {
  const { plainText, keyWord } = props;
  const tmp = plainText.split(keyWord);

  return tmp.map((item: string, index: number) =&amp;gt; {
    return (
      &amp;lt;span key={index}&amp;gt;
        {item}
        {index !== tmp.length - 1 ? &amp;lt;Text type=&amp;quot;danger&amp;quot;&amp;gt;{keyWord}&amp;lt;/Text&amp;gt; : null}
      &amp;lt;/span&amp;gt;
    );
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后再像这样渲染出来就行啦&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;function RETitle(props: any) {
  const { title, url, keyWord } = props;
  
  return (
    &amp;lt;Text ellipsis&amp;gt;
      &amp;lt;a href={url}&amp;gt;
        &amp;lt;RichText plainText={title} keyWord={keyWord} /&amp;gt;
      &amp;lt;/a&amp;gt;
    &amp;lt;/Text&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Windows和Linux共享蓝牙鼠标</title><link>https://site.j10ccc.xyz/zh-cn/blog/share-mouse-zh-cn/</link><guid isPermaLink="true">https://site.j10ccc.xyz/zh-cn/blog/share-mouse-zh-cn/</guid><pubDate>Thu, 16 Jan 2020 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;当一台电脑同时安装了Windows&amp;amp;Linux双系统（本文以Deepin15.11为例）时，每当切换到另一个操作系统，蓝牙鼠标便需重新配对，十分不方便。&lt;/p&gt;
&lt;p&gt;这是因为蓝牙设备有一个配对密钥，计算机必须有这个密钥，才能与蓝牙鼠标配对。而两个操作系统上保存的配对密钥不同，因此每次切换系统都需要重新配对。&lt;/p&gt;
&lt;p&gt;我们要做的便是设法令两个操作系统对蓝牙鼠标存有相同的配对密钥。&lt;/p&gt;
&lt;h2&gt;流程&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;在Linux中删除已配对的蓝牙鼠标并找到获取连接密钥&lt;/li&gt;
&lt;li&gt;在Windows中修改原密钥并配对蓝牙鼠标&lt;/li&gt;
&lt;li&gt;重启至Linux&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;1. 在Linux中配对&lt;/h3&gt;
&lt;p&gt;这一步前提是在Windows下，蓝牙鼠标可以正常使用。&lt;/p&gt;
&lt;p&gt;重启至Linux，在系统设置中连接蓝牙鼠标&lt;code&gt;设置-- 蓝牙--开启--扫描设备--连接&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;完成后，蓝牙鼠标在Linux系统中应该可以正常使用了。无论重启/待机，配对都不会丢失——除非再次按下了配对按钮。&lt;/p&gt;
&lt;h3&gt;2.获取Linux中的配对密钥&lt;/h3&gt;
&lt;p&gt;Linux将蓝牙设备的配对信息存放在 &lt;code&gt;/var/lib/bluetooth/{计算机的蓝牙MAC}/{蓝牙设备的MAC}/info&lt;/code&gt;中，需切换到root用户访问&lt;code&gt;/var/lib/bluetooth&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;$ su #切换到root用户，如果你是第一次使用root账户，那首先要重设置root用户的密码
$ cd /var/lib/bluetooth/{计算机的蓝牙MAC}/{蓝牙鼠标的MAC}
$ cat info
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;找到以&lt;code&gt;Key=&lt;/code&gt;开头的一行，后面的便是配对密钥。将其记录下来。&lt;/p&gt;
&lt;p&gt;如果&lt;code&gt;/{计算机的蓝牙MAC}/&lt;/code&gt;下有多个蓝牙设备MAC目录，可根据每个目录中info文件中的Name=开头一行判断哪个是蓝牙鼠标。&lt;/p&gt;
&lt;h3&gt;3.修改Windows中的配对密钥&lt;/h3&gt;
&lt;p&gt;重启至Windows，Windows系统将蓝牙设备的配对密钥存放于注册表的&lt;code&gt;HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\BTHPORT\Parameters\Keys\{本机的MAC}&lt;/code&gt;，但这个路径无法直接用&lt;code&gt;regedit.exe&lt;/code&gt;查看或编辑，下面有一种解决方法：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;通过此链接下载PsTools&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;解压后，将其中的PsExec.exe扔到&lt;code&gt;C://Windows//System32&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在有管理员权限的Powershell中，运行注册表编辑器:&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-fallback&quot;&gt;PsExec.exe -s -i regedit.exe
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;手动进入&lt;code&gt;HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\BTHPORT\Parameters\Keys\{蓝牙鼠标的MAC}&lt;/code&gt;，找到右侧名称与蓝牙鼠标MAC一致的项目，将其数值改成之前记录下的配对密钥。&lt;/li&gt;
&lt;li&gt;开关一次飞行模式，或将蓝牙关闭再打开，便可正常与蓝牙鼠标配对。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;结语&lt;/h2&gt;
&lt;p&gt;至此，如无意外，只要不再次按下配对按钮，蓝牙鼠标与两个操作系统的配对都不会丢失——无论重启/待机/切换系统，蓝牙鼠标都能在启动后直接使用。&lt;/p&gt;
</content:encoded></item><item><title>致22届某个高三班级</title><link>https://site.j10ccc.xyz/zh-cn/blog/to-seniors-zh-cn/</link><guid isPermaLink="true">https://site.j10ccc.xyz/zh-cn/blog/to-seniors-zh-cn/</guid><pubDate>Sun, 01 Aug 2021 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;h3&gt;本文的前言&lt;/h3&gt;
&lt;p&gt;前几天被老师要求给母校现高三某个班级写一篇经验分享，结合真实经历呕心沥血写了2000+字，结果有几句真心话被老师复查后要求删除，实在不忍心，故将原稿发至博客。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;以下是原稿&lt;/p&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;本人被浙工大不太好的专业录取，成绩也不太好，无奈被老师拉来写经验分享，但下文讲的都是我和我们班其他同(da)学(lao)高三复习的真实情况&lt;s&gt;和规划&lt;/s&gt;，所以读了本篇文章绝对血赚。先提一下考试规划吧，我的选课是物理地理技术，生物首考难不难冲我不知道，反正你们技术，和英语最好第一次过掉，物理上80+可以了（不知道一分一赋这个成绩还顶不顶），首考扔掉一两门差不多了。&lt;/p&gt;
&lt;p&gt;下面按学科分享经验&lt;/p&gt;
&lt;h2&gt;语文&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;高三上很多人都会把数学和语文放一边，专攻七选三，这种做法可取，学校里安排的考试也是只有七选三+英（我这届），&lt;s&gt;但是语数老师不太推荐&lt;/s&gt;，所以对于大部分同学，语文的在校学习时间只有课堂和早晚读，这些时间不能浪费，不然下学期赶语文进度很累的！&lt;/li&gt;
&lt;li&gt;前四题多刷题，拼音题练习时记录下不熟悉的词，53上了解区分方法（根据意思分辨多音字读音等），高三上学期早晚读都可以读53拼音，下学期以刷题为主&lt;/li&gt;
&lt;li&gt;标点题也以刷题为主，53里面虽然拿例子讲了一堆，要真的搞懂还得回到千奇百怪的题目中去&lt;/li&gt;
&lt;li&gt;病句题也是从题目里面掌握套路的（我归纳过大概10多种语病？遇到不会的一个一个排下来基本就对了）&lt;/li&gt;
&lt;li&gt;文言文课内的重点词和句式要非常熟悉，文言文前两个选择题挺重要的，还有翻译尽可能拿6分左右，具体复习还是早晚读去读，如果高二有每课总结的笔记最好是读自己的笔记&lt;/li&gt;
&lt;li&gt;古诗基本知识搞清楚（比如体裁和题材），赏析题除了掌握集中模板套路外，还要结合往年高考题，看看答案怎么答的（前面的阅读理解也差不多，名校、联考题大部分都是基于真题答案的）&lt;/li&gt;
&lt;li&gt;论语如果以前没认真听懂的，高三会有复习课一定要认真听，里面出现的重要字词（老师要求翻译的）最好摘录下来，还有课后题，每篇的主旨大意（有个别篇里面是分块的，比如说诲人不倦），这些笔记下学期复习的时候可以背一背，当看到句子能反应出出自哪篇（里的哪节）的时候，这块基本上就玩明白了&lt;/li&gt;
&lt;li&gt;作文高三上学期以积累素材为主吧，当然老师布置的练习还是要认真写的&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;数学&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;同语文学习的时间少，平时的考试要认真对待，选择题填空题里面的难题最好放一下，抓住其他的题目钻研一下（因为大部分压轴题的思路比较死板，方法掌握了对以后做题帮助微乎其微），有很多泛用的技巧老师以前赶课时的时候可能没讲到或者也是一带而过，同学们都没掌握（比如说极化恒等式，阿氏圆，三角形四心衍生结论等），平时考试或者练习里面应该会涉及到大部分，这些如果放到下学期去学，不花大量时间刷题巩固根本记不住&lt;/li&gt;
&lt;li&gt;选择填空（除最后一题），三角函数，立体几何，高三下学期通过刷题基本上能做全对，其他大题分数都是比较好拿的（按点给分），这样算下来120+是没有问题的，所以数学比较差的同学也不要觉得数学很难，掌握基础知识了，发挥正常110左右一定会有&lt;/li&gt;
&lt;li&gt;练习圆锥曲线前要知道往年高考的给分标准（老师应该有），按往年答案模板写题（哪怕你过程中有一点错误）效率是最高的，没人能保证你能算完或者证明完，平时养成这种习惯，先把能拿的分拿到（虽然练习时老师只看结果不看过程），考试的时候才能做到不浪费一分钟在圆锥曲线上&lt;/li&gt;
&lt;li&gt;高三下学期最好在晚读开始前让优秀的同学讲一下题目，有利于知识的传播，&lt;s&gt;减少圈子垄断&lt;/s&gt;，难题答案里的解题方法很可能没有同学的思路好，并且下学期数学作业量太大（2天-一张卷子？），老师很可能来不及讲 &lt;s&gt;有些老师也不见得会把最好的方法教给你们&lt;/s&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;英语&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;字不好的同学安排午休结束时练字&lt;/li&gt;
&lt;li&gt;词汇量不大的同学要紧跟老师的步伐，高三上学期会把整本维克多过一遍（大概12月初结束），这段时期是最好的记忆时间（同学都跟你背一样的单词可以互相抽测，并且每个星期都有同步练习检测），因为是按首字母排序的，所以会遇到长得很像的单词，建议写本子上隔天看一下加以区分&lt;/li&gt;
&lt;li&gt;晚读听力是隔天听一次，对听力不太自信的同学可以自己买本练习补上没听的那天（MP3偷偷听？让老师把音频导到云作业？），听感要每天保持&lt;/li&gt;
&lt;li&gt;听力书最好根据自己情况买（百朗比较难？维克多比较简单？），并且多听听不同嗓音的材料（今年考的都是烟嗓）&lt;/li&gt;
&lt;li&gt;客观题先做统考题（金考卷），积累点课外词汇、词组（一定要记，可以结合维克多），高考前全做真题适应一下，顺便攒点自信&lt;/li&gt;
&lt;li&gt;语法填空错题可以摘错题本上，包括复杂的句子结构，词组搭配，还有单词奇怪的变化&lt;/li&gt;
&lt;li&gt;作文注重平时积累，多写多看，可以每个星期加练应用文或者续写（概要），拉着老师让他改&lt;/li&gt;
&lt;li&gt;应用文种类笔记本上列全，练习遇到一种文体摘下范文大体格式（第一、三段，还有第二段的衔接句），文中第二段详细的例子也一起摘录下来，早晚读，考前突击比较省事，应用文 = 模板 + 例子&lt;/li&gt;
&lt;li&gt;班里同学的作文，周考别班大神的作文的亮点可以多抄几句（心理、动作描写，风景）自用&lt;/li&gt;
&lt;li&gt;可能老师会推荐续写技巧书，个人觉得没什么用，写作的时候多用平时摘抄的好词好句就行了，合理的运用词句加上合理的剧情18分+不难，续写 = 少量而显眼的优美描写 + 合理的剧情 + 合理的衔接&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;物理&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;刷题练选择题速度，必修3-4，3-5的知识点不会的先找老师问清楚再写&lt;/li&gt;
&lt;li&gt;班级里或者学校里考试的时候不会写的大题跳过，平时练习的时候最好做到不留空&lt;/li&gt;
&lt;li&gt;选择题和大题第一遍做题要看仔细，物理考试的时间很紧张，没时间回过头看的&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;技术&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;信息刷题连速度，题目要看仔细&lt;/li&gt;
&lt;li&gt;ps,flash,excel这些弄不清楚的最好摘录到错题本，然后去问老师或者自己在电脑上试一下&lt;/li&gt;
&lt;li&gt;通用选择题遇到怪题先放一放，选择题做完了再回头看一下，第一遍看的时候可能看漏了一些条件&lt;/li&gt;
&lt;li&gt;电控题，三视图熟能生巧&lt;/li&gt;
&lt;li&gt;画图题要看清题干，抓住得分要点，把重点画详细，尺寸标注尽量多标（4个？），拿个4分+可以了&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题外话&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;关于下学期的三位一体，好一点的学校（学考分数折合6A+的）一般没7，8个A进不了面试，所以刚好达到大学三位一体最低要求并且无竞赛获奖的同学可以考虑放弃，三位一体在4月份下半月，这个时候好多地区一模试卷都出来了，参加这个挺浪费时间的，学考强的同学当然随便上，高考加10~20分还是香的&lt;/li&gt;
&lt;li&gt;我这一届高三上学期每个星期天下午有周考，下学期基本上每天都有一门科目的小考，考试心态要放宽，学习的时间要合理安排好&lt;/li&gt;
&lt;li&gt;学校期末可能会购买一些联盟试卷，有些出题比较离谱，就当乐呵乐呵，千万别败坏了兴致&lt;/li&gt;
&lt;li&gt;晚自习老师会过来，白天没弄懂的点可以一次性上去问了，“充分利用好老师这个资源”&lt;/li&gt;
&lt;li&gt;老师讲题来不及会选题目讲，哪怕你觉得全班就你一个人不会也要举手，班上肯定有人没有完全懂的，至于讲不讲那是老师的事情了，下课同学肯定也会乐于帮助的，要记住最后证明自己的不是平时怕麻烦附和的一句“哦我懂了”，而是高考成绩&lt;/li&gt;
&lt;li&gt;有些专业性很强的知识不必去深究，达到能做高考题的水平就行了，或者比较抽象的知识实在理解不了的也可以扔掉，钻研下去浪费时间，问同学老师也会被厌烦&lt;/li&gt;
&lt;li&gt;学校里可能会有一些学科讲座安排在放学的时间段，强烈建议挤出时间去听，&lt;s&gt;20年的最后一个晚上我就是在1604度过的，那次英语作文技巧讲的非常好！&lt;/s&gt;&lt;/li&gt;
&lt;li&gt;两次高考前一个星期如果有学军镇海杭二的押题卷一定要做，&lt;s&gt;别问我为什么知道的&lt;/s&gt;&lt;/li&gt;
&lt;li&gt;在学校学习的时候不要干其他的，玩的时候就不要想着学习了，高三不仅需要汗水和毅力，还需要乐观的心态和健康的身体&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>决定使用 meta 核心</title><link>https://site.j10ccc.xyz/zh-cn/blog/use-meta-for-tun-zh-cn/</link><guid isPermaLink="true">https://site.j10ccc.xyz/zh-cn/blog/use-meta-for-tun-zh-cn/</guid><pubDate>Fri, 03 Nov 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;最近几天 Clash 圈子里面闹的沸沸扬扬，我也是趁着风头给电脑换上了 meta 内核。Meta 我早就听说了，但是一直不知道有什么契机需要用 TUN 模式，这个星期我发现了一个合适的理由，下面来谈谈。&lt;/p&gt;
&lt;h2&gt;契机&lt;/h2&gt;
&lt;p&gt;简述一下我现在的上网环境：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;寝室内：树莓派做代理服务器，所有设备 HTTP 代理连接树莓派&lt;/li&gt;
&lt;li&gt;寝室外：大部分场景只有 Mac 需要上网条件&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于 IOS 和安卓系统，网络的代理设置是跟随 AP 的，而 Mac 则是跟随系统的，所以在 Mac 上切换网络后就需要手动去切换系统代理。我的场景下是在寝室系统代理连接树莓派，外出启动 ClashX，代理切换到本地端口。&lt;/p&gt;
&lt;p&gt;在电脑上不仅是系统代理需要切换，Shell 的代理也需要手动切换。之前配置树莓派代理的时候就考虑到了这点，写了个快捷指令来修改系统代理。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;当然，这个快捷指令可以改成根据地理位置，或者连接上某个 AP 自动启动&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.j10ccc.xyz/static/blog/RhgG28.png&quot; alt=&quot;1.png&quot;&gt;
上述方案解决了我的需求：降低设备连接数量（对于机场），虽然麻烦点，但是一个月多用下来也是可以接受的。直到前几天我遇到了一个问题。&lt;/p&gt;
&lt;p&gt;我在寝室配置好 Shell 代理的情况下，在本地启动了一个 Tomcat App，然后在寝室外访问一个接口（这个接口需要连接云服务器上的数据库），结果数据库连接超时。通过重新配置 Shell 代理，并重启 Tomcat 解决问题。究其原因可能是 JDBC 连接数据库的时候继续使用了启动时环境变量中配置的代理（http 代理到寝室内的树莓派）。如果一个程序在启动的时候记录了网络相关的环境变量，并在应用运行过程中一直用这个配置，那当电脑的代理环境变化后是会造成隐晦的网络问题。&lt;/p&gt;
&lt;h2&gt;选择 meta&lt;/h2&gt;
&lt;p&gt;一番思考后，确认了问题在环境变量的切换，寝室内和寝室外这套方案必须得要两个代理地址，无法避免切换。如果切换的层级在 Shell 这里，那就会出现上述环境变量的问题。现在有一个解决方案，系统代理和 Shell 代理一律连接 Clash 的端口，然后在 Clash 中切换寝室内和寝室外的配置。&lt;/p&gt;
&lt;p&gt;其实一开始我没有想这么透彻，在想到原有的方案不行的时候我第一反应就是换成 TUN 模式，因为虚拟网卡的方案更底层，对所有的应用透明。实践的审核又想到区分寝室网络和寝室外网络可以在 GUI 中选择不同节点来实现。写这篇文章的时候才发觉，单后者就能解决问题了，不需要 TUN 也能实现。&lt;/p&gt;
&lt;p&gt;其实 TUN 也没什么不好的，我删掉了 Shell 脚本中一些关于代理地址的声明和配置，Shell 启动脚本的可移植性更强了👍&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.j10ccc.xyz/static/blog/KONWQT.png&quot; alt=&quot;Meta 官方维护的 GUI&quot;&gt;&lt;/p&gt;
&lt;p&gt;meta 官方的 GUI 也具备切换后端功能，兼容 Clash 内核。我将 Web 页面以 PWA 的形式模拟成了一个 App，在 Mac 上能同时管理和监控本机的 meta 内核和树莓派上的 clash 内核。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.j10ccc.xyz/static/blog/fh4G5y.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;额外的 http 代理配置&lt;/h2&gt;
&lt;p&gt;添加一个 Proxy，回到寝室的时候开启全局代理，节点选择 &lt;code&gt;dormitory&lt;/code&gt; 即可。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;proxies:
  - name: &amp;quot;dormitory&amp;quot;
    type: http
    server: 192.168.1.30
    tls: true
    port: 7890
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;macOS 系统服务 - root&lt;/h2&gt;
&lt;p&gt;从编译 meta 内核到开机自启，其中在 macOS 添加服务是最让我头疼的，尤其是这个服务得由 Root 启动。&lt;/p&gt;
&lt;p&gt;macOS 配置系统服务我就不阐述了，&lt;a href=&quot;https://www.jianshu.com/p/d6f09bc4142e&quot;&gt;这篇文章&lt;/a&gt; 讲的很透彻。但是文章中的例子不适用于 Root 服务。&lt;code&gt;plist&lt;/code&gt; 的创建者是当前用户，而不是需要执行该服务的 &lt;code&gt;root&lt;/code&gt; 用户，参考了 &lt;a href=&quot;https://superuser.com/a/547869&quot;&gt;这个回答&lt;/a&gt; 后我才成功配置好 meta 内核服务。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;现在这套联网方案，已经实现了较为优雅的代理切换。在设备齐全的工作室和环境灵活外出场景，理论上都能兼顾良好的网络访问性和资源的节约。&lt;/p&gt;
</content:encoded></item><item><title>有多套 JS 主题色变量了？来看看如何优雅使用它们！</title><link>https://site.j10ccc.xyz/zh-cn/blog/use-reactive-theme-token-in-js-zh-cn/</link><guid isPermaLink="true">https://site.j10ccc.xyz/zh-cn/blog/use-reactive-theme-token-in-js-zh-cn/</guid><pubDate>Sun, 30 Jun 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Web 开发中我们习惯了在浏览器中切换顶层 DOM 的 Class Name 来作为主题色的切换，这个方案依赖于 CSS 变量，更深入讲则依赖于浏览器实现的 CSS 规则。如果换个开发环境，换到了 React Native，或者其他不支持 CSS 的环境，那么主题色的切换就只能靠 JS 了。本文就是介绍如何使用 JS 实现一个优雅的主题色切换功能。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!Tip]
为了保证教程类文章的可读性，本文以“问题”来驱动思路的展现，先给出最终的实现效果，逆向推出实现思路。这也是阅读别人代码的正常方式。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;如何定义优雅&lt;/h2&gt;
&lt;p&gt;在某个 React Native 仓库中我发现了这样的代码&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// @filename: page.style.ts

import themeColor from &amp;quot;@my-charming-design/theme-color&amp;quot;
import StyleSheet from &amp;quot;@my-charming-design/style-sheet&amp;quot;

const createStyleSheet = () =&amp;gt; StyleSheet.create({
  container: {
    backgroundColor: themeColor.Background
  },
  intro: {
    color: themeColor.Title
  }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// @filename: page.tsx

function PageImpl() {
  const styleSheet = createStyleSheet();

  return (
    &amp;lt;View style={styleSheet.container}&amp;gt;
      &amp;lt;Text style={styleSheet.intro}&amp;gt;Hello word! (exactly)&amp;lt;/Text&amp;gt;
    &amp;lt;/View&amp;gt;
  );
}

export default function HelloWordPage() {
  return (
    &amp;lt;ThemeProvider&amp;gt;
      &amp;lt;PageImpl /&amp;gt;
    &amp;lt;/ThemeProvider&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码对应的两个样式(BackgroundColor &amp;amp; Color)，已经实现了 Light/Dark 主题的自适应。&lt;/p&gt;
&lt;p&gt;乍一看就看到的简单之处&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;仅仅声明了一处 Context，消费了一处 &lt;code&gt;themeColor&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;而对比 RN 正常写法来细看&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用了自己的 StyleSheet，并且没有在 FC 中直接消费，而是包了层函数，在 FC 中调用才得到实际的样式变量。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Context 用来共享 theme 状态可以理解，而声明样式的手段仅是写出对应的样式名称，样式代码只和样式名有关，和有多少套主题无关，这就非常简洁！整体的改动几乎没有。至此，这段 demo 就是本文讨论的优雅。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!Question] #1
为什么一个对象的属性，能代表出多种主题下的不同值？&lt;/p&gt;
&lt;p&gt;提示：例子中 &lt;code&gt;themeColor.Title&lt;/code&gt; 的类型是 &lt;code&gt;string&lt;/code&gt;，单独给 Title 拿出来能打印，能作为 inlineStyle。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;统一多套样式到一处&lt;/h2&gt;
&lt;p&gt;第一道题的答案是对象的 getter 属性。&lt;/p&gt;
&lt;p&gt;访问对象的一个属性，会触发 getter 函数，在里面判断出当前上下文是哪个主题，返回对应的样式就行。这里使用 Proxy 来实现。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// 这段代码为了展示清晰，改写了一些类型定义
const themeInstance = {
  mode: &amp;quot;light&amp;quot;
}

const dark = defineToken({
  Title: &amp;quot;#e4e4e4&amp;quot;,
  Background: &amp;quot;#1f1f1f&amp;quot;
})

const light = defineToken({
  Title: &amp;quot;#101010&amp;quot;,
  Background: &amp;quot;#ffffff&amp;quot;
})

const colors: Record&amp;lt;&amp;quot;light&amp;quot; | &amp;quot;dark&amp;quot;, string&amp;gt; = {
  light,
  dark
}

const themeColor = new Proxy&amp;lt;ReturnType&amp;lt;typeof defineToken&amp;gt;&amp;gt; (colors[themeInstance.mode], {
    get: (_, key: any) =&amp;gt; colors[themeInstance.mode][key]
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从一个全局变量中 读取/修改 当前主题，之后访问 themeColor 的时候就能拿到最新的主题变量，主题色控制的部分差不多写完了，于是很轻松就写出了下面的最简 demo 来测试效果（&lt;code&gt;ThemeProvider&lt;/code&gt; 和 &lt;code&gt;useColorMode&lt;/code&gt; 的实现过于简单，就不详细展开了）。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;export default function ThemeProvider(props: { children: ReactNode }) {
  const { children } = props;
  const [mode, setMode] = useState(&amp;quot;light&amp;quot;);

  return (
    &amp;lt;ThemeContext.Provider value={{ mode, setMode }}&amp;gt;
      {children}
    &amp;lt;/ThemeContext.Provider&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;function useColorMode() {
  const themeContext = useContext(ThemeContext);

  /**
   * 修改 context 中的 state，保证子组件重新渲染
   * 同事修改全局状态，保证 themeColor 能拿到最新值
   */
  function setColorMode(mode: string) {
    themeInstance.mode = mode
    themeContext.setMode?.(mode)
  }

  return {
    mode: themeContext.mode,
    setColorMode
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;function InlineStyleContainer() {
  const { mode, setColorMode } = useColorMode()

  function handleToggleColorMode() {
    if (mode === &amp;quot;light&amp;quot;) setColorMode(&amp;quot;dark&amp;quot;)
    else setColorMode(&amp;quot;light&amp;quot;)
  }

  return (
    &amp;lt;&amp;gt;
      &amp;lt;section style={{ height: &amp;quot;100px&amp;quot;, backgroundColor: themeColor.Background }}&amp;gt;
        &amp;lt;h1 style={{ color: themeColor.Title }}&amp;gt;Title: InlineStyle&amp;lt;/h1&amp;gt;
      &amp;lt;/section&amp;gt;
      &amp;lt;button onClick={handleToggleColorMode}&amp;gt;Toggle Color Mode&amp;lt;/button&amp;gt;
      &amp;lt;div&amp;gt;Current color mode: {mode}&amp;lt;/div&amp;gt;
    &amp;lt;/&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!Question] #2
真的成了吗？&lt;/p&gt;
&lt;p&gt;提示：组件已经包裹 ThemeProvider。ThemeProvider 中维护了一个 state，useColorMode 透传state 能力，并完成对全局主题变量的更新。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;文章还要写下去，那第二道题的答案显而易见了。到底哪里不对？梳理一下，&lt;code&gt;themeColor&lt;/code&gt; 取值的变化是因为组件重新渲染，重新渲染是因为 &lt;code&gt;ThemeProvider&lt;/code&gt; 中触发了 state。子组件的主题样式更新，必须由顶层组件的更新触发，换句话讲，子组件没有能力感知主题状态的变化。&lt;/p&gt;
&lt;p&gt;这下问题大了，子组件一旦包裹在 &lt;code&gt;React.memo&lt;/code&gt; 中，并且 props 不包含主题变量，那这个组件的样式就不是自适应的了。&lt;/p&gt;
&lt;h2&gt;完成 真-响应式&lt;/h2&gt;
&lt;p&gt;拍脑袋一想，之前观察 demo 代码的两个点，有一个实现漏了。那就是 demo 自己实现了一个&lt;code&gt;StyleSheet&lt;/code&gt;，在 FC 中还包裹着一层函数，调用后消费。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!Question] #3
为什么要在 FC 中消费，直接在 FC 外调用拿到值，或者干脆不包函数，在 FC 中直接消费值不行吗？&lt;/p&gt;
&lt;p&gt;提示：什么函数必须在 FC 中调用？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;先猜测吧。hook 必须在 FC 中调用，猜测包了层函数只为在 FC 中调用 hook。回去看看&lt;a href=&quot;#%E5%A6%82%E4%BD%95%E5%AE%9A%E4%B9%89%E4%BC%98%E9%9B%85&quot;&gt;代码&lt;/a&gt;，&lt;code&gt;StyleSheet.create&lt;/code&gt; 就是 hook，而上文的实现中和 hook 有关的只有个 &lt;code&gt;useColorMode&lt;/code&gt; ，先思考仅用这个能不能解决问题。&lt;/p&gt;
&lt;p&gt;先搭个架子&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;function useThemeStyle&amp;lt;T&amp;gt;(style: T): T {
  useColorMode();
  // TODO: Question#4 答题占位
}

const StyleSheet = {
  create: useThemeStyle
}

export default StyleSheet;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!Question] #4
在 TODO 注释补完代码&lt;/p&gt;
&lt;p&gt;提示：把这个函数补完就能在 React.memo 中实现组件级别的响应式样式了&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;最后一百米&lt;/h2&gt;
&lt;p&gt;第四题答案很简单。先来梳理一下，这个 hook 就是 &lt;code&gt;create&lt;/code&gt; 函数，返回值得是主题样式对象。函数的入参就是我们要返回的对象，似乎对入参做些处理就能返回了。&lt;/p&gt;
&lt;p&gt;再来回顾问题，我们希望在主题变量 &lt;code&gt;mode&lt;/code&gt; 变化后重新渲染一下 FC。create 是 hook，hook 中的 useColorMode 在主题变化的时候会触发 FC 渲染，hook 中只要调用了 &lt;code&gt;useColorMode&lt;/code&gt; 就已经实现功能了。所以直接返回 &lt;code&gt;style&lt;/code&gt; 就行了。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-diff&quot;&gt;function useThemeStyle&amp;lt;T&amp;gt;(style: T): T {
  useColorMode();

+  return style;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;眼见的同学发现了一个&lt;strong&gt;加分项&lt;/strong&gt;，可以给 &lt;code&gt;mode&lt;/code&gt; 拿出来，判断外层是不是包了 &lt;code&gt;ThemeProvider&lt;/code&gt;，这样在独立的组件中直接使用 &lt;code&gt;StyleSheet.create&lt;/code&gt; 就会有提示出现。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-diff&quot;&gt;function useThemeStyle&amp;lt;T&amp;gt;(style: T): T {
  const { mode } = useColorMode();

+  if (!mode) {
+    console.warn(&amp;quot;Please use theme in ThemeProvider.&amp;quot;)
+  }
  
  return style;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;到这里就实现完成了，具体的代码在我的仓库中能看到，&lt;a href=&quot;https://github.com/j10ccc/theme-token-demo-react&quot;&gt;j10ccc/theme-token-demo-react&lt;/a&gt;，里面有文中对应场景的 demo 可以调试观察。&lt;/p&gt;
</content:encoded></item><item><title>毕业之后关于学海平板的处置</title><link>https://site.j10ccc.xyz/zh-cn/blog/xuehai-tablet-reborn-zh-cn/</link><guid isPermaLink="true">https://site.j10ccc.xyz/zh-cn/blog/xuehai-tablet-reborn-zh-cn/</guid><pubDate>Mon, 21 Jun 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;高中毕业学海终于给平板解 MDM 了，备份好以前的课件和照片后，马上准备双清然后 root （这Android9 动画bug太多了。。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A：为什么要 ROOT ？&lt;/p&gt;
&lt;p&gt;B：这 3G RAM 的平板日常怎么用？！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;本文应该是全网首发，嘿嘿~&lt;/p&gt;
&lt;h2&gt;开始前注意&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;本文参考&lt;a href=&quot;https://forum.xda-developers.com/t/samsung-galaxy-tab-a-8-0-2019-with-s-pen-lte-sm-p205-root-achieved-howto.3971209/&quot;&gt;XDA前辈的文档&lt;/a&gt;，并结合网络上各类教程做了大量改动&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;刷机前请备份好重要资料，可以下个S换机助手备份&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;学海解锁后，国行系统ota更新不更新随意&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;想好刷什么系统，国行最新 Android9 ，港版 Android11（下图），&lt;s&gt;国行原味省电，港版卡的一🖊（虽然优化后还过得去，UI比较现代化）&lt;/s&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;港版 Android11 &lt;strong&gt;无法降级&lt;/strong&gt;，所以选择 ROM 时请谨慎&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;解锁 Bootloader 后每次开机会出现警告，强迫症介意请别 ROOT&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.j10ccc.xyz/static/blog/6bbp2S.png&quot; alt=&quot;6bbp2S.png&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;准备&amp;amp;约定&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;SM-P200设备一台，系统 Android9&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://jamcz.com/sambox.html&quot;&gt;晨钟酱三星工具箱&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://m.samsungmembers.cn/thread-1030503-4-473.html?ivk_sa=1024320u&quot;&gt;固件下载器hadesFirm 0.3.6.1&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;开始&lt;/h2&gt;
&lt;h3&gt;下载固件&lt;/h3&gt;
&lt;p&gt;开 hadesFirm，型号 SM-P200 ，国行区域CHN（港版TGY），检测更新，如图设置，点击下载（下得挺快的，比某mobile快多了&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.j10ccc.xyz/static/blog/G7Jo8Q.png&quot; alt=&quot;G7Jo8Q.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;下好之后解压，有 AP，BL，CSC，HOME_CSC开头的4个文件&lt;/p&gt;
&lt;h3&gt;刷机前准备&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;平板解锁开发者选项，打开&lt;strong&gt;USB调试&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;USBA连接电脑（不要Type-C连电脑！）打开晨钟工具箱，被检测到进入软件页面后，就算连接成功了&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.j10ccc.xyz/static/blog/MRQxLo.png&quot; alt=&quot;MRQxLo.png&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;点击&lt;strong&gt;下载端口驱动&lt;/strong&gt;，解压安装就行了&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;开始刷机&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;点击&lt;strong&gt;打开Odin刷机工具&lt;/strong&gt;，根据弹出提示清除所有账户（如果有），如果驱动安装成功的话中间一排小格（可能不是第一格）显示蓝色（如果不是，请重启电脑）并且 Log 中输出&lt;code&gt;Added!&lt;/code&gt;，这样表示平板被Odin检测到了&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.j10ccc.xyz/static/blog/5ai4M5.png&quot; alt=&quot;5ai4M5.png&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Odin中选项切换到 Opinions ，取消选择 Auto Reboot&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;其他不用更改，如图导入文件(BL导入BL开头文件，AP导入AP开头文件，CSC导入CSC开头文件，HOME_CSC文件暂时用不到)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.j10ccc.xyz/static/blog/Z7kbwL.png&quot; alt=&quot;Z7kbwL.png&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;保持连接电脑，重启平板，在黑屏的时候按住 &lt;strong&gt;音量+&lt;/strong&gt; 和 &lt;strong&gt;音量-&lt;/strong&gt;，进入 Download Mode，根据提示短按音量上进入刷机模式（看到这界面突然发怵不敢刷的，可以长按 &lt;strong&gt;音量-&lt;/strong&gt; 和 &lt;strong&gt;关机键&lt;/strong&gt; 重启回系统）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;点击 Start 开始刷机，如果中途出现失败（Odin 中 Log 中出现&lt;code&gt;Fail&lt;/code&gt;），点击 Restart 重刷，或者换线、换电脑另一个USB插口重刷（失败后虽然不能进入系统，但是平板连接电脑依然能被Odin检测到），成功之后会显示一个绿色的大大的 &lt;strong&gt;PASS&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;准备ROOT&lt;/h3&gt;
&lt;p&gt;只想体验港版 Android11（ OneUI 3.1 ）的小伙伴到这里就可以结束了，国行 Android9 或者 港版 Android11 想 root 的接着看&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;设置向导随便弄点，到时候还要清空数据，设备连接互联网（应该不用科学上网，最近移动宽带放的有点开，说不准要不要翻墙）&lt;/li&gt;
&lt;li&gt;下载并安装 &lt;a href=&quot;https://github.com/topjohnwu/Magisk/releases&quot;&gt;Magisk Manager最新版&lt;/a&gt;，导入刚才的AP开头的文件到平板文件目录任意位置&lt;/li&gt;
&lt;li&gt;打开 Magisk Manager ，在 &lt;code&gt;Magisk&lt;/code&gt; 那块点击安装，接着选项栏里&lt;code&gt;安装到Recovery&lt;/code&gt;不要勾，点击下一步，方式选择&lt;code&gt;选择并修补一个文件&lt;/code&gt;，然后选择导入的AP开头文件，等他修补完成，会在设备根目录下的 Download 文件夹下生成 patched文件，重命名为&lt;code&gt;magisk_patched.tar&lt;/code&gt;，导入到 PC电脑&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;解锁Bootloader&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;设备打开开发者选项中的&lt;strong&gt;OEM解锁&lt;/strong&gt;，重启至 Download Mode ，根据设备提示，长按 &lt;strong&gt;音量+&lt;/strong&gt; 进入解锁界面，然后短按解锁 Bootloader&lt;/li&gt;
&lt;li&gt;解锁后设备自动重置，正经地完成设置向导，进入系统先重新安装 Magisk Manager&lt;/li&gt;
&lt;li&gt;打开设置，解锁开发者选项，保证OEM解锁选项还存在，如果发现OEM解锁不见了（至少在 Android11 上是这样的），请下载&lt;strong&gt;crom1.08&lt;/strong&gt;，Android11 打开闪退，然后重启，即可看见OEM是开着的，并且是灰色的（这里感谢 酷安网友@EdwardW 的实测，不然我也不确定这个是不是巧合）&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;开始ROOT&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;从晨钟工具箱中打开 Odin ，跟之前一样关闭 Auto Reboot ，BL选择BL开头的文件，&lt;strong&gt;AP选择刚才修补的&lt;code&gt;Magisk_patched.tar&lt;/code&gt;&lt;/strong&gt; ， &lt;strong&gt;CSC选择HOME_CSC开头的文件&lt;/strong&gt;，点击 Start，刷好之后进入系统即是 ROOT 状态&lt;/li&gt;
&lt;li&gt;打开 Magisk Manager ，会显示修复Magisk，点击修复即可&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;尾声&lt;/h2&gt;
&lt;p&gt;至此学海平板 SM-P200 的 ROOT 教程到此结束，ROOT后干嘛不用我多说，&lt;s&gt;Scene，LSPosed跑起来！！&lt;/s&gt;，ROOT给的权限卸载系统应用能让系统更加省电，流畅。&lt;/p&gt;
&lt;p&gt;有几个遗憾：Scene因处理器冷门无调度方案，充电详细信息无法读取，优化后内存平均占用2.1G（比 Android9 多了0.4G，别说我不会优化！从 Moto 到 Pixel 玩机经验还是很足的），每次通知栏下拉会卡 600ms（可能动画就这样 &lt;s&gt;每看到这个血压马上上来了&lt;/s&gt;）&lt;/p&gt;
&lt;p&gt;精简One UI，给这台&lt;s&gt;高价低配的&lt;/s&gt;平板一个清楚的定位——Notebook，用户应用仅留下三星笔记（Spen延迟极低，不知道 Android9 延迟怎么样），Autodesk SketchBook，WPS Office，via浏览器（实测一点都不卡）。&lt;/p&gt;
&lt;p&gt;也许，当今社会下，被性能制约的平板才是真正的生产力工具！&lt;/p&gt;
</content:encoded></item></channel></rss>