前端

📝 15 篇文章
📅 最新: 2024/12/19

小圆季节灏文档|小圆季节灏信息码获取

# 小圆季节灏 GitHub地址:https://github.com/charlesix59/QDU_AFL_V3 ## 获取信息代码 其实原理非常简单,只需要您填写一个json文件,格式如下: ```json { "name":"你的名字", "number":"你的学号", "sclass":"你的班级", "collage":"你的学院", "tname":"你导员的大号", "tel":"你的电话", "address":"你要去的地方", "dormitory":"你的住处" } ``` 复制一份到任意文本编辑器,修改各个值为你需要的值,然后复制,粘贴到小程序中的信息码文本框即可 ## 更新感言 腾讯真的恶心,直接填个人信息无法过审,所以我就只能曲线救国了。 信息代码的获取方法在操作指南中 乞望见谅 2022.9.3 <hr> 有些事说是小心也好,说是形式也罢。 但是体现在个人身上,是痛苦与气愤。 我对如是行径感到恶心。 虽然大多是为了自己,如果下次又有魑魅魍魉在人间横行,我依然会用自己的方式祛魅。 2022.9.2 <hr> 某大学终于打开了虚妄的门扉,推倒了染血的藩篱。这是一场胜利,或许是。 有幸看到<b>**小圆**</b>在此刻退休,目前仍未更新的版本也不会再继续更新了。 对于小圆的退休我并不感到遗憾 > 但愿世间人无病,何妨架上药生尘。 我们还有别的战场要奔赴,哪里有压迫,哪里就有反抗。不合理仍然存在,以真理之名,我仍要与这些恶魔战斗。 无论如何,很高兴看到这一片光辉闪烁着,我们可以暂时唱着欢歌,相拥而眠;不必枕戈待旦,横刀冷对了。 今天,它退休了,但当黑暗再次笼罩之时,我仍将燃起炬火,予你微弱的光芒。 5.26 <hr> 最近校园集结号迁移到微信小程序了,于是我又赶出来一个。 因为是微信小程序,大家能在微信中直接搜索到我就不上传gitee了 从网页到mui到微信小程序这都第三版了,考虑要不要直接uniapp……(那我就快要成为真正的集结号了) ### 早期版本 V2版本:[校园集结号自主请假: 勾勾啊哈哈 (gitee.com)](https://gitee.com/charles-min/QDU_ASLv2) V1版本:[charlesix59/QDU_self_ASF_web (github.com)](https://github.com/charlesix59/QDU_self_ASF_web) ### 一些废话 > 沉默呵,沉默呵!不在沉默中爆发,就在沉默中灭亡——鲁迅 > 哪里有压迫哪里就有反抗——毛泽东

修复fluid的tags中的词云tag跳转异常问题

# 修复fluid的tags中的词云tag跳转异常问题 ## 前言 最近在尝试搭建自己的blog,再再三考量之下选择了**hexo**,不得不说这个blog框架还是很香的,配合github的托管能够快速搭建一个界面美观的博客。 但是在使用的过程中我发现了一个小问题,就是博客的tags并不能正常工作。 当我们从首页中的tag跳转时,可以跳转到正常的界面,如图: ![](../../essay/prose/imperial_edict/img1.png) <font color=red>但是当我从tags界面的词云中跳转时就会出现错误</font> ![](fluid-tags-bug-fix/img2.png) 错误界面如图: ![](fluid-tags-bug-fix/img3.png) <!-- more --> ## 解决方法 <font color=red>我们可以很明显的观测到问题——url地址错误,重复了一次'tags/'目录</font> 我们首先说解决方案: <font color=red>找到目录`<your hexo dirctory>\node_modules\hexo\lib\plugins\helper`中的tagcloud.js文件,将文件第71行改为:</font> ```javascript `<a href="${url_for.call(this, tag.path).substring(5)}" style="${style}"${attr}>${transform ? transform(tag.name) : tag.name}</a>` //原代码为: //`<a href="${url_for.call(this, tag.path))}" style="${style}"${attr}>${transform ? transform(tag.name) : tag.name}</a>` //加上了一个子字符串分割 ``` ***注意!这个方法只适合跟我描述的错误相同的朋友,如果问题不同请不要随意更改!*** ## 详细解释 这一部分讲述我怎么分析并解决问题的,如果不感兴趣可以直接关掉网页了 首先发现到这一点后,我企图弄清楚fluid是如何生成tags这个html文件的。但是我观察文件夹时并没有和明显的察觉到这一点。于是我找到`public`文件夹,**这个文件夹存放的是通过hexo生成的html文件。** 然后我们找到`tags`文件夹下的`index.html`   文件,随着翻阅html文件,我发现了这样的一块代码: ```html <div class="text-center tagcloud"> <a href="tags/node-js/" style="font-size: 15px; color: #bbe">node.js</a> <a href="tags/前端/" style="font-size: 15px; color: #bbe">前端</a> </div> ``` 毫无疑问,这就是生成词云的代码,但是我已经生成为html的文件我们并不能直接改,因为如果要这样解决问题,每次生成新的html之后我们都要手动修改一次,这是很没有效率的。于是我们顺藤摸瓜继续寻找问题。 于是我在webstrom使用<kbd>ctrl</kbd>+<kbd>shift</kbd>+<kbd>r</kbd>全局查找tagcloud,发现在fluid的layout中有一个名为`tags.ejs`文件,这显然是生成tags的文件之一。其中有这样的一块代码: ```ejs <div class="text-center tagcloud"> <%- tagcloud({ min_font: min_font, max_font: max_font, amount: 999, unit: unit, color: true, start_color, end_color }) %> </div> ``` 很明显,这是生成上面那个html的映射文件,但是这其中依然没有修改herf的方法。于是我们继续向源头寻找。 然后我们发现tagcloud包是被引用进来的,而他的源码就在我们面提到过的文件夹中。 ```javascript `<a href="${url_for.call(this, tag.path)}" style="${style}"${attr}>${transform ? transform(tag.name) : tag.name}</a>` ``` 这一行很明显就是生成每个小tag的代码。虽然我们看不太懂`${url_for.call(this, tag.path)}`这行代码的意思,但是我们知道最终他都会被解析为字符串。如果你熟悉js,就会知道js拥有将所有东西转化为字符串的能力😂因此我们直接使用substring方法来更改这一行代码就可以解决问题了

修復hexo fluid归档、分类、标签界面查看文章404错误

# 修復fluid归档、分类、标签界面查看文章404错误 ## 前言 上次遇到了[tag跳转的问题](https://charlesix59.github.io/2022/09/01/fluid-tags-bug-fix/)之后,我又遇到了这个奇怪的问题。按照按图索骥的原则,我又用那篇文章中提到过的方法进行了纠错,最终也是顺利的解决了这个bug。 错误如图: ![](../../essay/prose/imperial_edict/img1.png) <!-- more --> ## 解决方案 <font color=red>还是和以前一样,先说结论</font> <font color=orange>警告⚠:如果你的错误和我的不同,请不要随意修改</font> 首先找到fluid主题的目录中的`<your fluid dir>/layout/_partials` 打开`category-list.ejs`文件 修改第**45行**为 ```html <a href="../<%= url_for(cat.path) %>" style="text-align: center" class="list-group-item list-group-item-action"> ``` 修改第**50**行为 ```html <a href="../<%= url_for(post.path) %>" title="<%= post.title %>" ``` 然后打开`archieve-list.ejs` 修改第10行为 ```html <a href="../<%= url_for(post.path) %>" class="list-group-item list-group-item-action"> ``` 然后**复制**整个文档,**新建**一个名为`sub-archive-list.ejs`(名字可以自定)的文件,将复制的内容**粘贴**过去 并修改第10行为 ```html <a href="../../<%= url_for(post.path) %>" class="list-group-item list-group-item-action"> ``` 然后回到上一级目录,找到`tag.ejs`,修改第12行引用的文件为你新建的那个文件: ```ejs <%- partial('_partials/sub-archive-list.ejs', { params: { key: page.layout, postTotal: tag ? tag.posts.length : 0 } }) %> ``` 同时如上修改`category.ejs`中的第12行为: ```ejs <%- partial('_partials/sub-archive-list.ejs', { params: { key: page.layout, postTotal: cat ? cat.posts.length : 0 } }) %> ``` ## 其他 我不知道是不是因为我配置不完全或者安装出现问题才导致有这么多的bug,但是随着我的重装这个问题也没有得到很好的解决。 如果你也遇到了相似的问题可以尝试用这种方法解决 如果你对详细的解决思路感兴趣,你可以查看[前言](#前言)中提到的文章

IndexDB游标的正确打开方式

# IndexDB游标的正确打开方式 ## 引言 我最近在维护我的玩具项目`react-door`,当我试图使用IndexDB的游标遍历某个表时,我遇到了一些让人十分困惑的问题,下面我将带大家来复现一下事故的现场。 ## 复现 首先,在我的印象中,游标类似迭代器,所以刚开始我试图拿到游标传递出去然后在dao层调用: ```js // indexDB.js // 获取cursor const getCursor = () => { if (!db) { return Promise.reject("database connect instance can't be null") } const store = db.transaction(storeName, 'readonly').objectStore(storeName) const request = store.openCursor(); return new Promise((resolve, reject) => { request.onsuccess = (e) => { const cursor = e.target["result"] resolve(cursor) } request.onerror = (e) => { reject(e) } }) } // in dao.js async function someFunc() { const cursor = await getCursor(); if (cursor) { // do something cursor.continue() } else { console.log("no more data") } } ``` 一开始我是这么设想的,但是我发现了一个令人特别疑惑的问题:为什么在MDN的文档中,使用`if...else...`来遍历这个cursor呢? ```js if (cursor) { var listItem = document.createElement("li"); listItem.innerHTML = cursor.value.albumTitle + ", " + cursor.value.year; list.appendChild(listItem); cursor.continue(); } else { console.log("Entries all displayed."); } // 来自MDN,可以看到使用 if else 进行遍历 ``` 当时我也是借鉴MDN,使用`if...else...`来遍历,但是我发现我的代码只能运行一次,这肯定不叫遍历啊。**谁家好人用`if...else`做遍历啊???** 年轻人不气盛能叫年轻人吗?这肯定是MDN写错了!根据我的经验,立刻给他改写成了这样: ```js async function someFunc() { const cursor = await getCursor(); // 从 if 语句变为 while 语句 while (cursor) { // 猜猜会输出什么? console.log(cursor.key); // do something cursor.continue() } console.log("no more data") } ``` 看起来没什问题,非常正确! **但是** ,在我实际运行这段代码的时候,并不能正常运行。我们希望它的输出是`1 2 3 4 5 ....`,而实际的输出结果却是`1 1 1 1 1 ...`。为啥会出现这种结果呢?😥本来我以为是`cursor.continue()`方法是异步的,但是结果这是一个完完全全的同步函数。怎么回事呢? 如果说`continue`方法不起作用的话,但是它确实能够帮我们将cursor向前移位,并在cursor结束时结束循环(虽然报了一个info),但是如果说它起作用的,我们的cursor每次都指向第一条数据。这是怎么回事呢??? 难道IndexDB这个方法还有bug?不可能吧?*难道真的是使用`if...else`遍历*?于是我再次查阅MDN,现在我为大家贴上MDN完整的代码段: ```js function displayData() { var transaction = db.transaction(["rushAlbumList"], "readonly"); var objectStore = transaction.objectStore("rushAlbumList"); objectStore.openCursor().onsuccess = function (event) { var cursor = event.target.result; if (cursor) { var listItem = document.createElement("li"); listItem.innerHTML = cursor.value.albumTitle + ", " + cursor.value.year; list.appendChild(listItem); cursor.continue(); } else { console.log("Entries all displayed."); } }; } ``` 经过我的观察,难道...`cursor只能在openCursor的回调中使用??` 带着这个猜想,我将我的代码改为这样: ```js if (!db) { return Promise.reject("database connect instance can't be null") } const store = db.transaction(storeName, 'readonly').objectStore(storeName) const request = store.openCursor(); return new Promise((resolve, reject) => { request.onsuccess = (e) => { const res = [] const cursor = e.target["result"] if (cursor) { res.push(cursor.value) cursor.continue() } else { resolve(res) } } request.onerror = (e) => { reject(e) } }) ``` 嗯,果然不出所料,现在我们已经可以正确的遍历cursor了!但是又有一个问题随之而来,为什么每次我的res只有一个数据😅?经过我的一番思考,一个很恐怖的想法闪现出来,沃日,indexDB你不会....**每次游标移位都要调用onSuccess回调吧**? 于是我们再次改写代码,写成这样: ```js if (!db) { return Promise.reject("database connect instance can't be null") } const store = db.transaction(storeName, 'readonly').objectStore(storeName) const request = store.openCursor(); // 将res放到回调函数之外 const res = [] return new Promise((resolve, reject) => { request.onsuccess = (e) => { const cursor = e.target["result"] if (cursor) { res.push(cursor.value) cursor.continue() } else { resolve(res) } } request.onerror = (e) => { reject(e) } }) ``` 这时,我们终于如愿以偿的拿到了我们需要的数据(真是辛苦啊😭) ## 总结 现在我们来总结一下cursor的坑: - cursor必须配合`oepnCursor`函数的回调函数一起使用 - 每次调用`cursor.continue()`并不是简单的将cursor移位,而是请求IDB的API更新cursor并再次触发`openCursor`的回调函数 所以上面那一版代码就是使用cursor遍历的标准模板,因为这个过程是类似递归的过程,所以我们的cursor是真的使用`if...else...`遍历代码😥,没毛病哦兄弟们

使用node.js创建一个todo列表——node.js服务器搭建、json读写以及使用pm2保持服务运行

# 使用node.js创建一个todo列表——node.js服务器搭建以及json读写 ## 前言 前些日子学习了 nodejs ,顺理成章的想要找点东西练练手。恰好最近需要一个简介的todo list网页,因为之前用的一些todo list应用都被墙了,访问速度感人,于是就想自己搞一个todo。 而恰好,我要搞的todo是一个非常简单的程序,非常适合用nodejs这样的解释型脚本语言来实现,比起java这样光搭框架就要半天的专注大型项目的语言,nodejs 的优势就是非明显了。 <!-- more --> nodejs搭建服务器只需要去nodejs官网复制这样的一块代码: ```javascript const express = require('express') const app = express() app.get('/', (req, res) => { res.send('Hi!') }) app.listen(3000, () => console.log('Server ready')) ``` 所以我们可以把更多的经历放在我们的业务实现上。 ## 准备工作 ### 需求分析 在开始动工之前,要先列出需求。当时我的需求如下: - 添加需要做的事情 - 展示需要做的事情 - 完成需要做的事情 - 查看曾经完成的事情 ### 解决方案分析 对于上述需求,我打算这样实现: - 使用json文件系统实现持久化操作 - 使用nodejs来获取数据 - 使用nodejs来处理数据并储存数据 这样的话。明确了需求与实现方式,我们就可以动工了。 ## 实现 ### json读写 nodejs提供了一套文件的读写的模块:`fs`。使用fs可以很简单的实现文本文件的读写。但是为了我们在开发的时候更加方便,我又将他封装成了一个专注json读写的方法: ```javascript //in jsonHandler.js const fs = require("fs"); function test(){ console.log("Hello world!") } function readJson(name){ let jsonFile = fs.readFileSync("./"+name); let toDoList = JSON.parse(jsonFile); //解析json,并直接返回json对象 return toDoList; } function writeJson(name,data){ fs.writeFileSync("./"+name,JSON.stringify(data)); } module.exports = { readJson, writeJson } ``` ### 服务器搭建 还记得我们的服务器的格式吗?我们先定义一下我们的服务器接口: ```javascript //in index.js const jh = require("./jsonHandler") const http = require('http'); const url = require('url'); const util = require('util'); let workList = jh.readJson("list.json"); const port = 3000 const server = http.createServer((req, res) => { //设置响应头与请求编码 res.statusCode = 200 //设置字节流编码为utf-8 res.setHeader('Content-Type', 'text/plain;charset=utf8') //设置允许跨域 res.setHeader('Access-Control-Allow-Origin','*') req.setEncoding('utf8'); //write your code here let ret=""; //这是需要返回的数据 res.end(ret) }) server.listen(port, () => { }) ``` 然后根据我们的需求分析,先来确定以下API接口: - `addWork()`:用来添加一个任务并将数据保存到json - `deleteWord(res)`:用来将一个任务删除并将数据保存到json - `writeHistory(work)`:将完成的任务添加到历史记录中 - `getAllWork()`:获取目前正在运行中的任务 首先我们先开始addWork()的编写,要添加的话,首先要接受并解析客户端发送的请求,然后再储存到json中,代码如下: ```javascript //用来增加一个任务 function addWork(){ let work=""; req.on('data', chunk => { //接受客户端发送的请求流 //这里要取子串的原因是请求会带key,我这里是work=xxx,所以要将'work='去掉 work=chunk.toString().substring(5); //设置编码,不然中文会乱码 work = decodeURIComponent(work); //解析worklist let jsonObj=eval(workList) jsonObj.push({"work":work}) //将数据写回 jh.writeJson("list.json",jsonObj) }) } ``` 任务历史与添加任务几乎同理,只不过这个方法获取的数据是deleteWork提供的,我们不需要再去监控客户端的请求。代码如下: ```javascript //任务历史 function writeHistory(work) { let jsonStr = jh.readJson("history.json"); let jsonObj = eval(jsonStr); let event = {} event.work = work; event.finishTime =new Date(); jsonObj.push(event); jh.writeJson("history.json",jsonObj) } ``` 删除任务也是同理,我们要在前端给每个任务隐性的标上index,然后就可以用来删除了: ```javascript //删除任务 function deleteWork(res){ req.on('data', chunk => { console.log(`可用的数据块: ${chunk}`) //获取点击事件的位置 let index=chunk.toString().substring(3); let jsonObj=eval(workList) //添加历史 let work = jsonObj[index].work writeHistory(work) //删除 jsonObj.splice(index,1); // console.log(jsonObj) jh.writeJson("list.json",jsonObj) }) } ``` 获取任务就非常简单了,我们只需要从json中拿到数据并转发给客户端就可以了: ```javascript //获取所有任务 function getAllWork(){ workList=jh.readJson("list.json"); ret=JSON.stringify(workList); } ``` 最后呢,我们需要写一个路由解析,因为js非常小,所以这些东西都写在一个文件里面就可以: ```javascript /解析路由 let pathname = url.parse(req.url).pathname; //路由配置 if (pathname==="/getAllWork"){ getAllWork() } else if(pathname==="/addWork"){ addWork() } else if(pathname==="/deleteWork"){ deleteWork() } ``` 随下贴出前端源码: ```html <-- in index.html --> <!DOCTYPE html> <html lang="ch"> <head> <link rel="stylesheet" type="text/css" href="https://cdn.simplecss.org/simple.css"> <link rel="stylesheet" type="text/css" href="technological.css"> <meta charset="UTF-8"> <title>Title</title> <script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js"></script> </head> <body class="tech-background" style="text-align: center"> <p> wish you a substantial day</p> <p>to do list</p> <label> <input name="add-work" id="add-work" class="tech-input" style="width: 70%"> <button onclick='addWork()' class="tech-btn" style="display: inline;">增加</button> </label> <div id="container"></div> <script> 'use strict' $.ajax({ url:"http://localhost:3000/getAllWork", method:"GET", crossDomain: true, success:(res)=>{ let con=document.getElementById("container"); let workList=JSON.parse(res) console.log(workList) workList=eval(workList) for (let i=0;i<workList.length;i++){ let list=document.createElement("label"); list.innerHTML="<input name=\"work-list\" id=\"work"+i+"\" value='"+workList[i].work+"' class='tech-input' style=\"width: 70%\"> " + "<button onclick='deleteWork(this)' id='"+i+"' class='tech-btn' style=\"display: inline;\">完成</button>"; con.appendChild(list); } } }) function addWork(){ let work=document.getElementById("add-work").value; $.ajax({ url: "http://localhost:3000/addWork", method: "POST", crossDomain: true, data:{ "work":work }, success:(res)=>{ // alert("success"); location.reload(); } }) } function deleteWork(res){ let index=res.id; $.ajax({ url: "http://localhost:3000/deleteWork", method: "POST", crossDomain: true, data:{ "id":index }, success:(res)=>{ // alert("success"); location.reload(); } }) } </script> </body> </html> ``` 这个项目的源码我也会放在GitHub(因为项目实在太小我甚至都没有建一个文件夹) [项目地址](https://github.com/charlesix59/todo_list_by_nodeJS) ## 保持代码运行 我们服务器代码当然要有一个服务器去运行,而这个迷你的项目显然不值得让我们去分配他一个云服务器加一个域名,那么我们是否可以把这个服务运行在本地呢? 可是运行在本地就需要频繁的重启服务,每次开机都要启动一次,这未免也太麻烦了。于是我们就可以使用**pm2**来使我们的项目保持运行 关于pm2我在此不多赘述了,大家有兴趣的可以自行百度搜索,我只给出部署的命令: ```shell npm install pm2 -g #全局安装pm2 cd <项目目录> pm2 start index.js --watch pm2 save pm2 list ``` 如果你部署成功,那么当你运行`pm2 list`之后你就可以看到你的项目了

flex-direction为row时主动换行

# flex-direction为row时主动换行(React native主动换行) 我今天在写`react-native`时候,遇到了很不舒服的一件事情。因为RN默认是`flex`布局,所以如果我想要实现`inline`模式就只能设置父元素的`flex-direction`的value为`row`。这样如果我们如果想要换行需要怎么处理呢? 要知道,在RN中是没有`<br>`标签的,所以要想换行需要采取一些非常的手段。 那有没有什么好办法能够做到主动换行呢? 在正式开始之前我先上一个例子: ```jsx const arr = [1, 2, 3, 4, 5, 6]; export default function App() { return ( <SafeAreaView style={styles.container}> <View style={{ flexDirection: 'row' }}> {arr.map((item, index) => ( <View> <Text key={index}>{item}</Text> </View> ))} </View> </SafeAreaView> ); } ``` 很明显,上述的代码会生成一个纵向排列的`123456`,如果我们希望在**每三个断一下**,即将纵向的`123456`改成横向的`123 <br> 456`应该如何做呢? 刚开始的时候我问了ChatGPT,他给出的答案是使用伪类撑开元素: ```html <div class="flex-container"> <div class="flex-item">1</div> <div class="flex-item break-after">2</div> <!-- 想要在这个元素后换行 --> <div class="flex-item">3</div> <div class="flex-item">4</div> </div> ``` ```css .flex-container { display: flex; flex-wrap: wrap; flex-direction: row; } .flex-item { width: 100px; margin: 5px; } .break-after::after { content: ""; flex-basis: 100%; } ``` 经过我的尝试,这种方法很遗憾的不起任何作用,而且在RN中使用CSS class并不是很容易。但是他给出的代码确实给了我一些启发,经过一些摸索,我发现**如果一次渲染两个元素,其中一个是正常元素而另一个负责撑开元素**,那么就可以实现换行,代码如下: ```jsx const arr = [1, 2, 3, 4, 5, 6]; export default function App() { return ( <SafeAreaView style={styles.container}> <View style={{ flexDirection: 'row', flexWrap: 'wrap', width: 200 }}> {arr.map((item, index) => { if (item % 3 !== 0) { return <Text key={index}>{item}</Text>; } else { return ( <> {[ <Text key={index}>{item}</Text>, <View style={{ width: '100%' }}></View>, ]} </> ); } })} </View> </SafeAreaView> ); } ``` 但是很抱歉,这样依旧有缺陷,因为reactNative中`<></>`实际上会影响继承,即Text和View将会被看作一个整体,而不会被解析成两个单独的元素,**并且`return`中的`jsx fragment`必须有一个根节点**,也就说说这种方法与顶上charGPT给出的方法并没有区别,都无法将一个元素完美换行。这只会让我们需要换行的元素变为单独的一行而已。 那么,还有什么方法呢? ## 完美的换行 这时候我们或许需要更新一下思路,如果在渲染过程中无法做到完美的换行,那么我们为什么不能在数据上动一点手脚呢?经过一系列思路的转换,我突然想到我们完全可以**进行一个数组分割+元素嵌套的方式实现换行**,代码如下: ```jsx const arr = [1, 2, 3, 4, 5, 6]; export default function App() { const [arrState, setArrState] = useState([]); useEffect(() => { const result = []; let tempArr = []; arr.forEach((item) => { tempArr.push(item); if (item % 3 === 0) { result.push(tempArr); tempArr = []; } }); setArrState(result); }, []); return ( <SafeAreaView style={styles.container}> {arrState.map((subArr, index) => ( <View key={index} style={{ flexDirection: 'row', flexWrap: 'wrap', width: 200 }}> {subArr.map((item, subIndex) => { return <Text key={subIndex}>{item}</Text>; })} </View> ))} </SafeAreaView> ); } ``` 终于,我们实现了完美的换行! so,如果你也想要在flex布局中尝试主动换行,那么希望这篇文章能够解答你的疑惑。 当然,如果你有更好的方法,非常希望能在下方评论区和我分享你的好主意🤗

如何在 react 表达式中传入 react node[] (节点数组)

# 如何在 react 表达式中传入 react node[] (节点数组) 我们知道 jsx 表达式允许插入`reactNode`以及`ReactNode[]`,当但我们传入一个类似的东西的时候他却会报错: ```jsx <div className='App'> { <p>aaa</p> <p>bbb</p> // ❌ JSX expressions must have one parent element } </div> ``` 但是我们明明可以使用 map 函数传入数组呀! ```jsx <div className="App"> {arr.map((num) => ( <p key={num}>{num}</p> ))} </div> ``` 如果我们希望手动在表达式中传入一个节点数组我们应该怎么做呢?当我遇到这种奇葩问题的时候我也是愣了一会的,经过我对`Array.map()`方法的观察,我突然顿悟,如果想要手动传入一个节点数组应该这么写: ```jsx <div className="App">{[<p>aaa</p>, <p>bbb</p>]}</div> ``` 但是回头想想这个问题奇怪的很,我们明明可以写成: ```jsx <div className="App"> <p>aaa</p> <p>bbb</p> </div> ``` 但是其实有时候我们真的会需要手动的传入一个`node[]`,比如有些组件可能只接受一个节点数组而你又只有有限个节点不希望使用`map()`,或者是调试时候使用,再或者是希望在渲染列表中加入几个与之前的节点结构不同的节点,比如以下这个实例: ```jsx <div className="App"> {arr.map((num) => <p key={num}>{num}</p>) .concat([<p>aaa</p>, <p>bbb</p>])} </div> ``` 以上就是如何使用`reactNode[]`的一点个人心得咯

全面理解useEffect——useEffect用法解析与使用技巧

# 全面理解useEffect `useEffect`可以说是react的hook中最强大的一个,他能够以一种极为优雅的方式模拟我们的生命周期。但是优雅的代价就是这个hook极其的晦涩,不容易被理解与掌握。下面我们就来全方位的探讨一下,`useEffect`到底怎么用 ## useEffect的结构 在上一篇关于react的文章我提到过,函数式组件每次`reRender`都会重新执行一遍,如果我们需要生命周期的效果,那我们就需要`useEffect`函数。我们先来看一下useEffect的结构: ```js useEffect(setup, dependencies?) ``` - 参数 - `setup`:一个实现了你需要的副作用逻辑的函数,这个函数将会在组件首次被绑定以及每次**重新渲染导致`dependencies`更新时**调用。同时他还可以`return`一个清除函数,这个函数将会在函数卸载以及每次**重新渲染导致`dependencies`更新之前**调用 - `dependencies`:在`setup`中引用的所有`reactive values`(state、参数以及在组件内部定义的变量和函数)或更新凭据组成的数组。这个数组必须是有限的并且偏平的,每次更新react将使用`Object.is()`比较每个依赖项是否与之前相同,如果不同则更新。 - return - `undefinded` (无返回值) 中文博客中经常会漏掉`setup`函数的返回值,但是它的返回值是这个函数的半壁江山,我们决不能将其忽略不提,如果忽略掉的话我们就有一半的效果无法实现了。 ❗:`useEffect`也是一个hook,请在组件或者自定义hook的最上层使用,不要在循环或分支结构中定义hook ## 模拟生命周期 react生命周期大概分为三种,`mount`、`update`、`unmount`,当然在类式组件,我们已经可以规定是在mount之前,mount时还是mount之后调用,在函数式组件我们就不分的这么清楚了,因为在函数式组件我们的生命周期其实是独立的,是面向状态的。 下面我们就使用useEffect简单模拟一下类式组件的生命周期: **只在初始化时执行(`DidMounted`)** : ```js // 不依赖任何变量,则传入一个空数组 useEffect(() => { console.log("init"); return () => {}; }, []); ``` **每次更新时执行(`render`)** : ```js // 依赖所有变量,则不传入 useEffect(() => { console.log("rerender"); }); ``` **每次卸载时执行(`beforeUnmount`)** : ```js // 不依赖任何变量,return的函数将会在卸载之前被调用 useEffect(() => { return () => {console.log("unmount");}; }, []); ``` 实际上,我们经常说`useEffect`的作用是模拟声明周期,而这似乎违背了函数式组件的初衷,也违背了`useEffect`的初衷。我们应该多从**状态**的角度去思考,`useEffect`并非是什么生命周期函数,而是**状态更新**函数。 ## 控制更新 如果某个变量在每次组件更新的时候都需要更新,那我们完全没有必要使用`useEffect`,我们使用副作用钩子一定是因为我们需要控制某个变量的更新。 我们可以使用`useEffect`将**组件内部的状态与组件外部系统同步**: ```jsx // 比如说,我们可以在父组件更新props的时候调用useEffect const Child = ({ count }) => { const [childContext, setChildContext] = useState(0); useEffect(() => { setChildContext(count); }, [count]); return <p>this is a child, count = {childContext}</p>; }; ``` 或者我们可以使用`useEffect`将**组件组件外部系统与内部的状态同步**: ```jsx import "./styles.css"; import { useEffect, useState } from "react"; const fetch = async (count) => { // 假设这是一个网络请求 // 当然,这个外部状态也可以是一个外部组件,外部变量,jsAPI等等 await new Promise(() => { setTimeout(() => { console.log("fetch date"); }, count); }); }; export default function App() { const [count, setCount] = useState(0); const onClick = () => { setCount(count + 1); }; // 每次修改count都会将更改同步外部系统 useEffect(() => { fetch(count); }, [count]); return ( <div className="App"> <button onClick={onClick}>Change state in father</button> <p>count is {count}</p> </div> ); } ``` 其实生命周期也不过是对状态的统一更改,react现在只是将生命周期将颗粒度从组件降低到了状态。 ## 一些tips ### 在useEffect中使用state 有时候我们可能会需要在`useEffect`中使用和修改`state`,但是这是极端危险的行为,因为当我们修改`state`时会触发页面的`rerender`,然后因为`useEffect`依赖了`state`,这将会导致`state`被再次更改继而引起死循环…… ```jsx const Child = ({ count }) => { const [childContext, setChildContext] = useState(0); useEffect(() => { setChildConte(childContext); }, [count, childContext]); // !! 千万不要这么做,会引发死循环 return <p>this is a child, count = {childContext}</p>; }; ``` 如果我们在`useEffect`中根据之前的state更新state,我们可以这么写: ```jsx useEffect(() => { setChildContext(c => c + 1); }, [count]); // 不需要依赖childContext,安全 ``` 如果你真的既要在Effect中读取state,又要修改state,那么我给出的建议是,:**使用两个依赖同一个更新凭证`useEffect`** 千万别这么做: ```jsx useEffect(() => { setChildContext(Math.random()) console.log(childContext) }, [count, childContext]) // 死循环!!😭 ``` 正确的做法如下: ```jsx useEffect(() => { setChildContext(Math.random()) }, [count]); useEffect(() => { console.log(childContext) }, [count, childContext]) // 安全😊 ``` ### 消除不必要的对象与函数依赖 如果我们在组件内定义了一个函数或对象,我们在上一篇文章详细讲过,它每次都会被初始化。就像这样: ```jsx const childContext = { count: 1 } useEffect(() => { childContext.count = count; console.log("run effect") // 不要这样,每次Render都会触发effect😔 }, [count, childContext]); ``` 取而代之的是,如果你的对象或者函数只在`useEffect`中使用,则写在`setup`内部 ```jsx const Child = ({count}) => { useEffect(() => { const childContext = { count: 1 } childContext.count = count; console.log("run effect") }, [count]); // 推荐,只有在依赖更新的时候才会触发effect 😊 return <p>this is a child</p>; }; ``` 如果你是希望有一个在整个组件都可以被访问的静态变量,那么可以将对象写在组件外部: ```jsx const childContext = { count: 1 } const Child = ({count}) => { useEffect(() => { childContext.count = count; console.log("run effect") }, [count]); // 推荐,只有在依赖更新的时候才会触发effect 😊 return <p>this is a child</p>; }; ``` ## useLayoutEffect 我们在使用`useEffect`的时候有时会出现一种问题:我们的组件会发生闪烁或突然的变化。 这时因为`useEffect`是在`mount`之后执行的,因此第一次渲染出的值是我们定义的初始值,如果更新的性能比较差或者更新的范围比较大,可能会导致我们的`view`发生闪烁的现象。 这个时候我们就可以使用`useLayoutEffect`,他和`useEffect`的最大区别就是他是在`mount`之前执行的,所以不会导致`view`的闪烁。 但是如果我们没有很明显的ui闪烁问题,我们应该尽可能的使用`useEffect`,因为它的运行效率更好。

应该在哪里定义react函数——关于react开发规范的一些理解

# 应该在哪里定义react函数——关于react开发规范的一些理解 ## 前言 我之前有一个同事,从vue技术栈转入react技术栈,对于函数式编程还有一些不太熟悉。 最近他问了我一个问题,**react函数应该定义在什么地方**?他发现有些函数写在react组件之内,有些却写在react函数之外。他想知道哪种做法是最合适的。这是一个很好的问题,因为这体现了对于代码规范的追求。现在我希望在此妥善的梳理一下这个问题。 ## 看法 先开门见山的说一下我的看法,我的看法就是<mark>应外尽外</mark>,意思就是**能够**放到组件外部的函数都应该放到外部。但是需要注意的是,只有满足一定条件的函数才能够放到组件外部。在具体的讨论这些规则之前,我们应该先来看一下函数写在组件内与组件外部的<mark>区别</mark>。 ## 区别 其实两者的区别十分显著,我们在编写`jsx`文件的时候一般每个文件会暴露一个文件入口,也就是我们的组件函数。当我们在父组件使用我们的子组件的时候,实际上就相当于`new`出了一个新的组件实例。而在组件外的函数或者变量,则与组件形成了闭包。(不太了解闭包的可以理解为组件内的东西是原型模式,而组件外的东西是单例模式) 我们来看一个例子: 首先我们有一个Test组件: ```jsx let a = "123"; const Test = (props) => { return( <> <button onClick={()=>{console.log(a);a = props.value;}}>change</button> </> ) } export default Test; ``` 然后我们在App.js中调用这个组件: ```js function App() { return ( <div className="App"> <Test value={666}></Test> <Test value={777}></Test> </div> ); } ``` 可以看到我们在这里调用了两次Test组件,页面上应当有两个按钮。 这时我们应该想一下,当我们依次点击这两个按钮的时候输出的是 > 123 123 还是 > 123 666 呢? 答案显而易见,是`123 666`,各位可以自己去尝试一下。导致这种现象出现的原因其实不是react的组件,而是我们的`esm`。我们应该都对闭包有一定的概念,其实我们的模块化正是使用了闭包这一概念。当我们像上文那样编写Test组件的时候,其实闭包就已经形成了。 下面我们来梳理一下哪些函数是需要放到组件内的以及这其中的原因。 ## 需要在组件内的函数 ### Hook函数 一个hook函数只能在以下两个位置定义: 1. 自定义hook函数内部 2. React组件内部 但是其实我们的自定义hook最终也是在React组件内部调用,所以这就相当于每一个Hook函数最终都必须在React组件内部环境中执行。 ### 依赖Hook函数的函数 上文说过,hook函数只能在React组件内部调用,那么调用hook函数的函数也理所应当的需要写在组件内部。 ### 依赖State的函数 如果我们的函数需要直接使用State的值,那么我们需要将这个函数放置在组件内部,因为state是每一个组件实例私有的,在外部的函数无法读取到这个值。但是对于可能会复用的函数我们还是更推荐大家使用参数传递的方式传递state的值然后再使用setState的方式设置返回值。 不推荐: ```jsx // :( not recommend // a.jsx const [count,setCount] = useState(1); function a(){ setCount(count+2); } // b.jsx const [count,setCount] = useState(2); function b(){ setCount(count+2); } ``` 推荐 ```jsx // :) recommend //calcCount.js function calcCount(count){ return count+2; } //a.jsx const [count,setCount] = useState(1); setCount(calcCount(count)); // b.jsx const [count,setCount] = useState(2); setCount(calcCount(count)); ``` ### 依赖props的函数 依赖props的函数当然也不能将其写在组件外面。我们很大程度上是通过props去区别某一个组件函数的不同实例的,所以props也是组件实例私有的,需要写在外面。 ### 事件处理函数 虽然事件处理函数从理论上来说完全可以放置到组件外部(只要不依赖上面所说的),但是我们会认为事件处理函数是比较私人的东西,从习惯上来说我们还是更习惯将其放到组件的内部而不是抽出到一个util文件中,而且对于不同组件的事件处理函数,我们能抽象的部分也优先。 而且比起将事件处理函数抽象出来,我们更推荐将事件处理函数中处理逻辑的部分代码抽取出来,类似我们在MVC框架中所作的那样,model与controller的分离是值得推崇的设计思想。 ### 其他依赖组件实例的方法、变量的函数 其实我们可以看到,凡是我们的函数依赖了我们组件实例的私有变量、方法,我们都需要放在组件内部。 ## 为啥应外尽外? 以上我们整理出很多需要放到组件内部的情况,那为啥还推荐放到函数外部呢?直接全部放到函数内部不是会简单很多吗? 尽量将函数放到外面其实是一种函数式与模块化的设计思想,他驱使我们尽可能去复用我们的函数以及使用纯函数,综合来说,将函数与变量放到外部有如下几个好处: - 减少内存占用:减少组件内部的函数与变量可以减少组件实例中的内容,减少对内存的占用 - 重用代码:我们完全可以将一些负责逻辑、可以重用的函数归类并且放置到方法类(utils.js之类)中,减少我们的代码量 - 模块化:我们将逻辑与视图抽离完全可以减少我们组件的耦合度,并且可以降低模块体积,提高我们模块化的“效率”。而且能够使我们尽量去使用纯函数编写更优雅、更容易维护的代码 - 共享信息:我们可以刻意的通过将数据放置到组件外让这个组件的不同实例去共享信息。不过需要注意的是,我们放到组件外的数据肯定不是`React State`,这就意味着它们不是响应式的(一般不会有人这么做吧?) 综上所述,对于能够放置到组件函数外的应该尽量放置到函数外,对于能够复用的代码片,最好是能够抽取成全局的函数,这样才更符合模块化的要求。

为何不推荐在react组件中直接定义变量?探秘useRef的使用以及userRef与useState的区别

# 为何不推荐在react组件中直接定义变量?探秘useRef的使用以及userRef与useState的区别 ## 前言 今天在写代码的时候遇到一个问题,我的一个组件需要维护一个不需要在页面中渲染的变量。那么我想,既然它不需要渲染,我直接用`let`变量新建一个不就可以了吗? 但是当我真正用这个`let`创建的变量时,才意识到大事不好,原来这么使用变量有一个大问题! ## 为什么要有hook 要说明问题是什么,我们需要先弄明白一件事情,**我们使用函数式组件时我们怎么去`render`** ?在我们使用类式组件时,`rerender`可能是简单的调用一次`render`方法,那对于函数式组件呢?函数式组件的`render`写在`return`语句中,很明显我们需要重新执行整个函数。 那么我在前言部分提到的问题就呼之欲出了:组件每次渲染更新都会使得变量变为初始值。也就是说我在组件的一个函数中修改变量并更新组件,想在另一个函数中使用时我们获取到的就只会是初始值了。 可以看到,函数式组件是没有状态的!而react引入`hook`的原因就是补全函数式组件在这方面的缺陷。比如useState,当函数重新渲染时,state能够从状态池中获取到上次render的状态,像useEffect能根据变量来确定是否需要渲染,从而起到生命周期函数的作用。 也就是说,如果我们希望有一个能在每次渲染的时候不被重新初始化的变量,那么我们就需要借助hook了。 那么问题又来了,我们应该用哪一个`hook`呢?这里我推荐一个:`useRef`。 ## useRef 我们使用`useRef()`创建的变量不会每次都被重新渲染 ```js let count = 0 // 每次render都会init let count = useRef(0) // rerender时不会init ``` `useRef`的特性如下: - ref的值通过`current`属性获取 - ref是可以更改的 - 更改ref的值**不会**引起`rerender` - 如果ref的值被用于渲染,那么则不可更改 - 不建议在组件渲染中**读**写ref的`current`,推荐在`useEffect`与`event handler`中使用使用`ref.current` 可以看到,ref就是为我们保存**不用每次初始化**而且**不必渲染**的变量而生的。 ### useRef与useState 那么为什么我们要用ref而不去使用state呢?这两个hook有两个非常大的区别: - **ref不会引起rerender,而state会**。当我们调用setState时,会触发rerender,而在使用ref就不必担心这个,你可以随意的使用 - **ref是同步的,state是异步的**。当我们同步的使用useState更新数据之后,我们会获取不到最新的值,因为他是异步更新的。而ref则不会有这样的问题,你可以在更新之后立刻获取到最新的值。 所以对我现在的需求来说,`useRef`要比`useState`合适的多。 ### 使用useRef操作dom useRef的另一个重要的作用就是操作dom,我们在这边也顺带提一下吧。 我们可以这样使用ref: ```jsx const domRef = useRef(null) function focusInput(){ domRef.current.focus(); } return ( <> <input ref={domRef}/> <button onClick={focusInput}>click to focus<button> </> ) ``` 我们通过jsx中的`VNODE`的ref属性属性传递我们的ref,这样react会在渲染的时候自动将`current`的值设置为node。然后我们就可以访问到这个node了。 当这个node被从页面中移除时,`ref.current`也会被设置为`null` ## 总结 如果你希望在react函数式组件中保存一个不会每次渲染时更新并且无须被渲染的值,那么`useRef`是你的不二之选

解决插件使用GitHub calendar 插件无响应问题

最近开始发掘好用的hexo插件,先瞄准的是很久之前就看见过的小冰的GitHub Calendar [教程:hexo-githubcalendar 插件 1.0 | 小冰博客 (zfe.space)](https://zfe.space/post/hexo-githubcalendar.html) 但是按照他描述的方法配置完成之后,发现并没有生成Calendar,于是我就开始了漫长的排查。 <!--more--> ## 问题与解决方案 我这里先说我的问题,因为我是用了fluid主题,所以我先找到了小冰给出的fluid配置参数,实际上只需要将配置中的最后一行改为: ```yaml plus_style: "#github_container > .position-relative > .border{border:0!important}#github-calendar{position: relative;margin-top: -2rem;background-color: var(--board-bg-color);transition: background-color 0.2s ease-in-out;border-radius: 0.5rem;z-index: 3;-webkit-box-shadow: 0 12px 15px 0 rgb(0 0 0 / 24%), 0 17px 50px 0 rgb(0 0 0 / 19%);box-shadow: 0 12px 15px 0 rgb(0 0 0 / 24%), 0 17px 50px 0 rgb(0 0 0 / 19%);}" ``` 但是随着我按照教程完整的配置完全,发现并没有用,在对应插件的没有出现在它应该出现的位置。后来,觉得fluid有很多配置都是直接配置在`_config.fluid.yaml`中,我就尝试着将配置文件复制到这个文件一份,结果阴差阳错的成功了。 后来我又删除了hexo自带的config中的配置,结果又刷不出来了。 **所以,如果你是使用了某个主题,并且刷不出这个插件,<font color=red>你可以将主题和hexo的配置都加入插件配置</font>** ## 其他可能的问题 **这个插件想要使用应该不是只配置一个`_config.yaml`就可以了** ,你还需要在对应的界面(enable_page属性中对应的)加入相应的元素,如果是layout.type是id,那么就要插入: ```html <div id="这里替换为layout.name"> </div> ``` 如果你的layout.type是class,那么你要保证你那个界面又n-1个name为`layout.name`的代码块

基于命令模式使用 React 实现中文自动前进输入框(OTP)

# 基于命令模式使用 React 实现中文自动前进输入框(OTP) 最近写的项目中有一个需要自动换格子、自动聚焦并且让父组件获取输入值的需求。这个需求让我很头大,因为在 RN 中想要控制 DOM 绝非一件容易的事情。 ## 难点分析 - RN 控制 dom 相对麻烦 - 输入的是中文,需要处理输入法或复制粘贴的问题 - 内容需要和父子组件同步 - 格子数量不定长 - 每个 input 相对复杂 ## 解决方案 React 的单向数据流与组件化的模式一定程度上可以提高我们系统的可维护性,而另一方面这种设计也会导致我们的父子组件通信相对麻烦。在此处如果我们想要使用`ref`来维护一个 input 数组这显然是不现实的,因为我们**不能在 for 循环中定义`react hooks`**。而且我的每个 Input 都相对比较复杂,所以我选择每个 input 封装一个组件,然后在组件中使用`ref`来控制`vDom`。那么我们如何通信呢?自然就是**传参**咯。那么子组件如何优雅的通知父组件自身输入的改变呢?经过思索我想到了一种设计模式:<mark>命令模式</mark>。**通过命令模式+状态函数我们可以优雅的将子组件的变化通知父组件**。下面我们先来尝试实现一下最基本的功能,自动跳转与聚焦。 ### 自动聚焦 首先我们需要先思考,一个输入框可能有几种状态,其实无非两种`输入`和`删除`。但是我们是一组输入框,在其中来回跳,所以我们还需要一个`回退`状态。 ```ts // 定义命令格式 interface BaseCommand { name: string; value?: unknown; } interface CheckInputCommand extends BaseCommand { value?: string | undefined; additionalValue?: string; callarIndex: number; } ``` ```tsx // 父组件中定义命令状态 const [command, setCommand] = useState<CheckInputCommand>(); ``` 这样,确定了状态,我们还需要确定一件事情,我们的子组件要接受哪些参数呢? 首先,设置命令的状态函数一定是必须的,另外我们还需要传递一个下标来让组件知道它的序号以及一个`boolean`来表示是否聚焦,代码如下: ```tsx // 定义子组件 type propsType = { setCommand: Dispatch<SetStateAction<CheckInputCommand | undefined>>; index: number; focus: boolean; }; function InputCheck({ setCommand, index, focus, }: propsType): React.JSX.Element {} ``` 并且我们在父组件中应该如是调用子组件: ```tsx // 父组件 // 聚焦元素的下标,默认第一个元素聚焦 const [foucsElement, setFoucsElement] = useState(0); return ( // 此处省略n个字 <View> {arr.map((item, keyIndex) => { return ( <InputCheck key={`${index}-${keyIndex}`} setCommand={setCommand} index={keyIndex} // 下标与聚焦元素的下标相同则聚焦 focus={foucsElement === keyIndex} /> ); })} </View> ); ``` 通过上述的代码,我们已经能够得知我们如何来让一个元素聚焦了:**如果一个元素的下标与我们希望聚焦的元素下标相同则让他聚焦**。我们知道在 react 中,**如果 state 发生了变化则会引起发生变化的节点`rerender`,并且如果组件的 props 发生了变化会引起`reaction`的,这一系列反应让我们可以通过父节点的`state`来控制子组件。** 那么在子组件我们应该如何处理这种变化呢? 那自然是`useEffect`啊! ```jsx // 子组件 const [char, setChar] = useState(""); const inputRef = useRef<any>(); // 处理聚焦 useEffect(() => { if (focus) { inputRef.current.focus(); inputRef.current.setNativeProps({selection: {start: 1, end: 1}}); } }, [focus, setInputValue]); return ( <TextInput value={char} onChange={e => { TextInputHandler(e.nativeEvent.text); }} ref={inputRef} /> ); ``` 好的,这样我们就可以处理父组件传递的`focus`参数并且自动聚焦了。而且通过上面的代码我们也能发现我们返回的`TextInput`是一个<mark>受控组件</mark>哎~那我们又是如何处理输入的呢? 详情还请看代码: ```jsx /** 发送指令的函数 */ const sendCommand = useCallback( (name: string, commandValue?: string, additionalValue?: string) => { const command: CheckInputCommand = { name: name, value: commandValue, callarIndex: index, additionalValue: additionalValue, }; setCommand(command); }, [index, setCommand] ); /** 真正处理输入的逻辑 */ const setInputValue = useCallback( (str: string) => { setChar(displayChar); // 发送命令 sendCommand("input", commandStr, displayChar); }, [sendCommand] ); /** 处理输入 */ const TextInputHandler = (inputChar: string) => { setInputValue(inputChar); }; ``` 这样当我们输入的时候,子组件就已经能够发送命令通知父组件自己的变化了,这时我们就还需要父组件去监听子组件以及做出相应的反应咯: ```jsx // 父组件 /** 监听command变化 */ useEffect(() => { /** 接收子组件发送的命令 */ if (command && command.name === "input") { if (command.callarIndex === format.tunes.length - 1) { return; } setFoucsElement(command.callarIndex + 1); setChars(command.value || ""); } }, [command, format.tunes, format.tunes.length]); ``` 这样我们就可以自动的前进并聚焦啦! ### 处理中文 但是考虑到我们输入的是中文,所以我们必须要对输入进行一定的处理。首先是中文有输入法的问题,**这样我们就需要判断是否是中文,然后只对中文做出相应**。 ```ts // 中文匹配正则 const reg = /^[\u4E00-\u9FA5]+$/; /** 检查是否是中文 */ const verifyCharIsChinese = (char: string) => { if (reg.test(char)) { return true; } return false; }; ``` ```jsx // 子组件 /** 真正处理输入的逻辑 */ const setInputValue = useCallback({ if (!verifyCharIsChinese(str)) { return; } // 截取中文 let displayChar = str.substring(0, 1); let commandStr = str.substring(1); setChar(displayChar); // 发送命令 sendCommand("input", commandStr, displayChar); }, [sendCommand], ); ``` 另外因为中文会存在一次性输入多个字符的特点,所以我们就需要将输入的字符传递给父组件的问题。像上面的代码,我们已经看到我们已经截取了输入,然后发送到父组件,我们又该如何让父组件将多出的字符传递给剩下的子组件呢?**我想到的方法是:`useContext`!** ```tsx // 父组件 export const StrContext: Context<string> = createContext(""); fn(){ //省略函数定义 const [content, setContent] = useState<Array<string>>([]); // 保存真正的内容 if (command && command.name === "input") { useEffect(() => { if (command.callarIndex === format.tunes.length - 1) { return; } setFoucsElement(command.callarIndex + 1); setChars(command.value || ""); } } return( // ...此处省略n-m个字 <StrContext.Provider value={chars}> <View> {arr.map((item, keyIndex) => { return ( <InputCheck key={`${index}-${keyIndex}`} setCommand={setCommand} index={keyIndex} // 下标与聚焦元素的下标相同则聚焦 focus={foucsElement === keyIndex} /> ); })} </View> </StrContext> ) } ``` 同时我们**需要让得到焦点的子组件去处理我们的上下文**,即输入与重新发送指令: ```jsx const value = useContext(StrContext); /** 自动移动字符 */ useEffect(() => { if (focus) { inputRef.current.focus(); inputRef.current.setNativeProps({ selection: { start: 1, end: 1 } }); // 在上文的处理聚焦的函数中添加判断是否有全局字符串的方法 if (value) { setInputValue(value); } } }, [focus, setInputValue, value]); ``` ### 处理删除与后退 如果光前进却不能后退那不是没过河的<font color="green">卒</font>子嘛。看到如何处理前进那么如何处理后退应该也很明确了,但是需要注意,删除与后退并不一样, - **删除**:输入为空 - **后退**:输入已经为空并且继续删除 我们先来实现删除,删除非常简单,在处理输入的时候判断是否为空即可: ```jsx // 子组件 /** 真正处理输入的逻辑 */ const setInputValue = useCallback( (str: string) => { // 输入为空则发送删除命令 if (!str) { setChar(""); sendCommand("delete"); return; } if (!verifyCharIsChinese(str)) { return; } let displayChar = str.substring(0, 1); let commandStr = str.substring(1); setChar(displayChar); // 发送命令 sendCommand("input", commandStr, displayChar); }, [sendCommand] ); ``` <u>_父组件该如何处理我这里就掠过咯_</u>~ 处理后退要稍微复杂一些,首先我们需要在子组件中新增一个监听,监听我们的键盘输入: ```tsx // 子组件 /** 监听键盘事件,处理退格事件 */ const backSpaceHandler = (keyName: string) => { if (keyName === "Backspace" && !char) { sendCommand("back"); } }; return ( <TextInput style={TuneStyle} value={char} onChange={(e) => { TextInputHandler(e.nativeEvent.text); }} onKeyPress={(e) => { backSpaceHandler(e.nativeEvent.key); }} ref={inputRef} /> ); ``` 然后父组件也需要处理退格事件: ```jsx // 父组件 else if (command && command.name === "back") { if (command.callarIndex === 0) { return; } setFoucsElement(command.callarIndex - 1); } ``` ### 内容同步 哎呀,内容同步这种事情读者朋友们自己个性化发挥就好,毕竟我在命令模式中携带了挺多参数呢,请大家根据各自的需求随意发挥就好。 ## 完整代码 父组件: ```jsx export const StrContext: Context<string> = createContext(""); export default function fn(): React.JSX.Element { const [command, setCommand] = useState<CheckInputCommand>(); const [foucsElement, setFoucsElement] = useState(0); const [chars, setChars] = useState(""); // 多出的文字,自动填充到下一个block const [content, setContent] = useState<Array<string>>([]); // 保存真正的内容 /** 监听command变化 */ useEffect(() => { /** 接收子组件发送的命令 */ if (command && command.name === "input") { // 添加到内容 if (command.callarIndex >= 0) { setContent(e => { e[command.callarIndex] = addContentChar( command.additionalValue || "", command.callarIndex, ); return [...e]; }); } if (command.callarIndex === format.tunes.length - 1) { return; } setFoucsElement(command.callarIndex + 1); setChars(command.value || ""); } else if (command && command.name === "delete") { setContent(e => { e[command.callarIndex] = ""; return [...e]; }); } else if (command && command.name === "back") { if (command.callarIndex === 0) { return; } setFoucsElement(command.callarIndex - 1); } }, [command]); return ( <StrContext.Provider key={index} value={chars}> <View style={fillPoemStyle.inlineContainer} key={index}> {arr.map((item, keyIndex) => { return ( <InputCheck tune={item.tune} rhythm={item.rhythm} key={`${index}-${keyIndex}`} setCommand={setCommand} index={item.index as number} focus={foucsElement === item.index} rhymeWord={rhymeWord} /> ); })} </View> </StrContext.Provider> } ``` 子组件 ```jsx type propsType = { setCommand: Dispatch<SetStateAction<CheckInputCommand | undefined>>; index: number; focus: boolean; }; function InputCheck({ setCommand, index, focus, }: propsType): React.JSX.Element { const [char, setChar] = useState(""); const inputRef = useRef<any>(); const value = useContext(StrContext); const sendCommand = useCallback( (name: string, commandValue?: string, additionalValue?: string) => { const command: CheckInputCommand = { name: name, value: commandValue, callarIndex: index, additionalValue: additionalValue, }; setCommand(command); }, [index, setCommand], ); /** 真正处理输入的逻辑 */ const setInputValue = useCallback( (str: string) => { if (!str) { setChar(""); sendCommand("delete"); return; } if (!verifyCharIsChinese(str)) { return; } let displayChar = str.substring(0, 1); let commandStr = str.substring(1); setChar(displayChar); // 发送命令 sendCommand("input", commandStr, displayChar); }, [sendCommand], ); /** 自动移动字符 */ useEffect(() => { if (focus) { inputRef.current.focus(); inputRef.current.setNativeProps({selection: {start: 1, end: 1}}); if (value) { setInputValue(value); } } }, [focus, setInputValue, value]); /** 处理输入 */ const TextInputHandler = (inputChar: string) => { setInputValue(inputChar); }; /** 监听键盘事件,处理退格事件 */ const backSpaceHandler = (keyName: string) => { if (keyName === "Backspace" && !char) { sendCommand("back"); } }; return ( <TextInput style={TuneStyle} value={char} onChange={e => { TextInputHandler(e.nativeEvent.text); }} onKeyPress={e => { backSpaceHandler(e.nativeEvent.key); }} ref={inputRef} /> ); } export default InputCheck; ``` types ```ts // 定义命令格式 interface BaseCommand { name: string; value?: unknown; } interface CheckInputCommand extends BaseCommand { value?: string | undefined; additionalValue?: string; callarIndex: number; } ```