暂时未有相关云产品技术能力~
前言我现在的模式便是有问题就会先问问ChatGPT,甭管它给予我的答案是正确的、错误的、模棱两可的,都可以给予我一定的参考。 下面我主要来尝试一下,如何通过代码来实现微调呢? Fine-tuning - OpenAI API 这个链接算是官网的指导,其实我就是看着官网的指导进行的我的ChatGPT系列文章5、# ChatGPT开源的whisper音频生成字幕,可本地搭建环境运行,效果质量很棒4、开源组合llama+langchain,蹭上ChatGPT几行代码轻松搭建个人知识库私人助理聊天机器人3、惊奇,竟然可以在ChatGPT的GPT-4模型让它扮演Linux服务器 搭建K8s和docker环境2、从零开始构建基于ChatGPT的嵌入式(Embedding)本地医疗客服问答机器人模型1、GPT-4凌晨已发布赶紧申请,前端小白用最近用刚学的golang对接了GPT-3.5的6个接口准备环境可以先找到一个指定的文件夹,这里前提是要先安装 python如果执行没有看到版本信息要先去安装下载了,然后顺序执行以下指令// 安装依赖 pip install --upgrade openai // 设置 OPENAI_API_KEY export OPENAI_API_KEY="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" // 设置openai 代理 export OPENAI_API_BASE="https://service-r7s1v88u-1253646855.usw.apigw.tencentcs.com/v1"准备数据数据文件为test.jsonl,这里的数据我也不准备多了,对我来说只是实践一下如何来微调数据产生自己的模型而已。 下面是我随便准备了两条json数据{"prompt": "请使用golang写一个冒泡排序的算法", "completion": "这个算法不需要生成了,本地已经存在,请换其他问题"} {"prompt": "请告诉我XX公司发展的怎么样了", "completion": "融资款已到,XX项目的首付剩余款也在路上,已步入正轨"}重新格式化数据openai tools fine_tunes.prepare_data -f test.jsonl执行后会提供给我们一系列的建议,最后输出一个它建议的数据格式创建微调模型根据上面的数据进行创建微调模型,基础模型为 davinci,你也可以根据需要选择其他的模型。如果数据量很大,价格差距还是蛮大的openai api fine_tunes.create -t "test_prepared.jsonl" -m "davinci"看上图发现原来是stream流应该是不支持的, 这个是腾讯云的问题,于是我换成了使用cloudflare dash.cloudflare.com 搭建的代理然后再次运行立马成功了微调模型的调用第一种直接使用命令调用第二种使用接口调用后续通过官网 platform.openai.com/docs/guides…可以删除微调的模型可以对微调的模型继续微调可以对微调模型名称进行自定义等等还有其他许多的细节知识有兴趣的可以去看原版总结如何使用OpenAI fine-tuning(微调)训练属于自己的专有模型? - 知乎 (zhihu.com)这是知乎上一个大牛自己实测数据有兴趣的可以看一下。其实花费70、80美元还是蛮大的而且只是1000条数据而已,而且最终达到的效果并不是特别理想。Embedding模型其实在某些场景下的相似性、相关联结果还是非常棒的。对于微调模型暂时只能学习一下,感觉上并不能进行应用使用,或者使用的方法可能还存在问题,这里的实践仅供学术的参考。本文所有代码都在我的go代码仓库: github.com/aehyok/go-a…我的个人博客:vue.tuokecat.com/blog我的个人github:github.com/aehyok我的前端项目:pnpm + monorepo + qiankun + vue3 + vite3 + 工具库、组件库 + 工程化 + 自动化不断完善中,整体框架都有了 在线预览:vue.tuokecat.com github源码:github.com/aehyok/vue-…
1、前言我的ChatGPT 系列文章4、开源组合llama+langchain,蹭上ChatGPT几行代码轻松搭建个人知识库私人助理聊天机器人3、惊奇,竟然可以在ChatGPT的GPT-4模型让它扮演Linux服务器 搭建K8s和docker环境2、从零开始构建基于ChatGPT的嵌入式(Embedding)本地医疗客服问答机器人模型1、GPT-4凌晨已发布赶紧申请,前端小白用最近用刚学的golang对接了GPT-3.5的6个接口好了,那接下来看一下whisper开源库的介绍有五种模型大小,其中四种仅支持英语,提供速度和准确性的权衡。上面便是可用模型的名称、大致的内存需求和相对速度。如果是英文版的语音,直接想转换为英文。本来我是想直接在我的本地电脑上安装环境的,也就是无非安装python、ffmpeg、以及whisper,但是发现电脑配置太低了,而且我想测试一下large模型,CPU 肯定是不行,但是如果用本机的 GPU也是快不到哪里去的。 所以这里我想到谷歌的colab.research.google.com 免费在线运行,而且我可以启用GPU硬件加速,感觉上还是非常快的,当然如果需要你还可以购买。下面是我的免费配置 colab.research.google.com运行起来还是非常流畅,真的香喷喷,如果需要我都想付费了。可以应用于那些场景会议记录: 直接将录音转换为文字个人视频制作: 很多时候都希望有字幕的效果,听说剪映的效果都没有这个好课堂记录转写:将课堂上的内容记录下来,这样后面直接查看文字版本也是非常方便通话记录:有些重要的电话可将其录音,转换为文字以备后面查询也是非常不错的字幕组:这个就不用说了 有可能还涉及到多语言,准备率很高的话 可以省很多事情实时语音翻译:这个服务器配置够高的话,理论上就非常快速2、开始实践2.1、检查colab环境!nvidia-smi -L !nvidia-smi运行两个指令结果如下:1.!nvidia-smi -L:-L 参数用于列出系统上安装的所有 NVIDIA GPU 设备。运行此命令后,您将看到关于可用 GPU 的信息,包括其型号和 UUID。2.!nvidia-smi:不带任何参数运行 nvidia-smi 会显示有关 NVIDIA GPU 的详细信息,包括:GPU 设备的编号、名称、总内存和温度。GPU 使用率(如计算、内存和显存使用率)。运行在 GPU 上的进程以及它们的相关信息(如进程 ID、显存占用等)。只不过这里我还没开始使用GPU而已,所以显示的是空的。2.2、安装whisper!pip install requests beautifulsoup4 !pip install git+https://github.com/openai/whisper.git import torch import sys device = torch.device('cuda:0') print('正在使用的设备:', device, file=sys.stderr) print('Whisper已经被安装请执行下一个单元')这里主要就是安装whisper2.3、 whisper模型选择#@markdown # ** whisper Model选择** 🧠 Model = 'large-v2' #@param ['tiny.en', 'tiny', 'base.en', 'base', 'small.en', 'small', 'medium.en', 'medium', 'large', 'large-v2'] import whisper from IPython.display import Markdown whisper_model = whisper.load_model(Model) if Model in whisper.available_models(): display(Markdown( f"**{Model} model is selected.**" )) else: display(Markdown( f"**{Model} model is no longer available.** Please select one of the following: - {' - '.join(whisper.available_models())}" ))这里我选择的是最大的模型 large-v2,因为我要转换中文字幕,前面四个都只支持英文,这个在文章开头也说了的。2.4、 开始音频转字幕audio_path = "/content/downloads/test1.m4a" audio_path_local = Path(audio_path).resolve() transcription = whisper.transcribe( whisper_model, str(audio_path_local), temperature=temperature, **args, ) # Save output whisper.utils.get_writer( output_format=output_format, output_dir=audio_path_local.parent )( transcription, title )我首先要准备一个m4a的音频文件,这里可以直接上传到colab左侧当前目录是 content,然后右键新建文件夹downloads,然后在downloads文件夹上点击上传m4a文件上传完毕后可以看到m4a文件已经在目录下了。whisper.transcribe 方法有好多的参数whisper_model主要是设置model模型output_format 主要是设置字幕输出的文件格式temperature 值设置的较低,那么表述相对精准一些,值越大表述可能更加抽象一点args中有一个language语言,比如这里我要将音频转换为中文字幕 设置为cn 或者chinese这里主要可以查看 whisper/tokenizer.py at main · openai/whisper · GitHub2.4、运行查看效果点击运行后可以看到一段一段的在执行转换了,整体感觉运行还是非常流畅了,这比看别人在本地运行速度可是快多了最后可以看到srt字幕文件也已经生成了,可以直接点击左侧文件点击下载即可。 生成的srt文件如下3、总结这个whisper相当于离线版本,可以自己部署到本地或者服务器提供给自己使用,相信后续OpenAI应该还会有更新,提供更多精彩的功能使用。本文所有代码已上传github:go-openai/myai我的go代码仓库: github.com/aehyok/go-o…我的个人博客:vue.tuokecat.com/blog我的个人github:github.com/aehyok我的前端项目:pnpm + monorepo + qiankun + vue3 + vite3 + 工具库、组件库 + 工程化 + 自动化不断完善中,整体框架都有了 在线预览:vue.tuokecat.com github源码:github.com/aehyok/vue-…
1、前言我的ChatGPT 系列文章3、惊奇,竟然可以在ChatGPT的GPT-4模型让它扮演Linux服务器 搭建K8s和docker环境2、从零开始构建基于ChatGPT的嵌入式(Embedding)本地医疗客服问答机器人模型(看完就会,看到最后有惊喜)1、GPT-4凌晨已发布赶紧申请,前端小白用最近用刚学的golang对接了GPT-3.5的6个接口跟着步骤走,稍微懂点代码的都可以来操作,两个开源组件封装的非常彻底可以说拿来即用。所以无论作为前端也是可以来玩玩的。理想的状态就是,搭建好我的私人助理后,会将尽可能多的把数据投喂给它。本示例先来个结果给你们看看效果。我最后测试中文的时候加了两句话Interviewer:你每天睡多久? Interviewee:大概8个小时左右.来看看执行结果,我还掏出手机找到计算器 8*365=2920,还真是对的。对于这样的结果我真的非常满意,有时候准备投喂更多的数据来验证一下效果。初步来看:现代产品往往涉及很多来自不同渠道的用户调查数据,比如访谈、在线客服聊天记录、客户邮件、问卷调查和各种平台上的客户评价等。要整理和理解这些数据非常困难。通常,我们会把这些数据整理得井井有条,并用各种标签进行分类。但如果我们有一个人工智能聊天机器人,它可以回答所有关于用户研究数据的问题,那该多好呀!这个机器人可以通过查找大量的历史用户研究数据,为我们的新项目、产品或者营销活动提供有价值的见解和建议。那么接下来,我会用简单的代码,就可以实现这个简单的小目标,哪怕你没有技术背景也没关系。在这篇文章里,我会手把手教你如何去做。2、定制知识库首先我的想法是将我的个人数据作为研究数据,而不仅仅是互联网上的普通知识。前几天一直在看fine-tunes微调是否可以?但是后来发现了一个问题,微调是通过提供提示-响应示例来训练模型以特定方式回答问题。例如,微调有助于训练模型识别情感。要做到这一点,您需要在训练数据中提供句子-情感值对,就像下面的例子那样:{"prompt":"ChatGPT发布的GPT-4令人兴奋! ->", "completion":" 积极"} {"prompt":"湖人队连续第三晚让人失望! ->", "completion":" 消极"}但是我香提供的数据是不可能有提示-响应这样的示例。我们只有想用来寻找相关答案的数据。所以,在这种情况下,微调是行不通的。我想要的便是:需要让模型了解上下文。我们可以通过在提示本身中提供上下文来实现这一点。就像这样:.....各种上下文信息 .....各种上下文信息 .....各种上下文信息 .....各种上下文信息 .....各种上下文信息 回答问题: {用户输入的字符串}不过,有一个问题。我们不能把所有的研究数据都放在一个提示里。这在计算上是不合理的,而且 GPT-3 模型在请求/响应方面有 2049 个“令牌”的硬限制,虽然现在已经有了GPT-3.5以及GPT-4 我现在研究的还是GPT-3,后面看看是否可以进行优化使用最新的模型。我需要找到一种方法,只发送与问题相关的信息,以帮助我的私人助理回答问题,而不是将所有上下文相关都放在请求中,这样的花费实在太大了,也很不方便。3、实现好消息是,有一个名为 GPT Index 的开源库,由 Jerry Liu 创建,使用起来非常容易。github地址为:github.com/jerryjliu/l…以下便是它的工作原理:创建文本块索引找到最相关的文本块使用相关的文本块向 GPT-3 提问这个库为我们完成了所有繁重的工作,我们只需要编写几行代码。让我们开始吧!代码只有两个函数:第一个函数从我们的数据中构建索引,第二个函数将请求发送到 GPT-3。以下是伪代码:from llama_index import SimpleDirectoryReader, GPTListIndex, readers, GPTSimpleVectorIndex, LLMPredictor, PromptHelper from langchain import OpenAI import sys import os from IPython.display import Markdown, display def construct_index(directory_path): # set maximum input size max_input_size = 4096 # set number of output tokens num_outputs = 2000 # set maximum chunk overlap max_chunk_overlap = 20 # set chunk size limit chunk_size_limit = 600 # define LLM llm_predictor = LLMPredictor(llm=OpenAI(temperature=0.5, model_name="text-davinci-003", max_tokens=num_outputs)) prompt_helper = PromptHelper(max_input_size, num_outputs, max_chunk_overlap, chunk_size_limit=chunk_size_limit) documents = SimpleDirectoryReader(directory_path).load_data() index = GPTSimpleVectorIndex( documents, llm_predictor=llm_predictor, prompt_helper=prompt_helper ) index.save_to_disk('index.json') return index def ask_ai(): index = GPTSimpleVectorIndex.load_from_disk('index.json') while True: query = input("来,有问题先问我的AI助理吧? ") response = index.query(query, response_mode="compact") display(Markdown(f"Response: <b>{response.response}</b>")) if __name__ == "__main__": # 初始化OpenAI API KEY os.environ["OPENAI_API_KEY"]="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" 上面主要实现也就是两个函数的事情,一个是初始化数据的索引,另外一个函数便是输入字符串输出结果的问答模式调试这里发现了谷歌一个非常强大的工具,谷歌云盘,可以直接在上面调试运行代码,而且可以分步骤,真的不错。这是我在上面创建的测试项目,大家有兴趣的话都可以进行查看,还可以直接创建一个副本项目,根据自己的需求随便修改了,感觉这个云环境还是非常的方便,我这个项目的链接是:colab.research.google.com/drive/1yX4q…这里相当于重新创建了一个项目文件夹,然后在里面操作,相当于你的云系统只不过应该是linux的下载github项目,主要是下载我里面的模拟测试数据,安装python依赖准备代码,这个我上面实现的代码里拷贝过来就可以了设置一下自己的openai的api-key将准备的数据进行构建索引开启询问模式你还可以问不相关的问题,如果这样,他应该给不了你正确的答案的,所以语料你准备的越充足,那么在一定程度上是可以替代我的。我又问了一下 月薪是多少? 结果回复我与访问无关,没办法回复。也就是如果我加进去。现在我在data中添加如下信息Interviewer:What is your monthly salary? Interviewee:My monthly salary is $250.然后重新加载数据,再次去查询相关问题,我还是变相查询的,访谈中说的是月薪,这里我提问的是日薪,就是这么牛逼啊,这里你用中文来问也会得到同样的效果,文章开头我也添加了两行中文,然后去提问效果也非常不错。4、总结整体上这两个开源的类库,真的牛逼哄哄,简单的几行代码就可以让我们轻易的搭建起来这样一个本地的知识库,然后配合ChatGPT,可以再通过一个比较大的数据集来验证数据的质量。本文所有代码都在我的go代码仓库: github.com/aehyok/go-a…我的个人博客:vue.tuokecat.com/blog我的个人github:github.com/aehyok我的前端项目:pnpm + monorepo + qiankun + vue3 + vite3 + 工具库、组件库 + 工程化 + 自动化不断完善中,整体框架都有了 在线预览:vue.tuokecat.com github源码:github.com/aehyok/vue-…
1、前言我的ChatGPT 系列文章4、开源组合llama+langchain,蹭上ChatGPT几行代码轻松搭建个人知识库私人助理聊天机器人(测试发现质量不错)3、惊奇,竟然可以在ChatGPT的GPT-4模型让它扮演Linux服务器 搭建K8s和docker环境2、从零开始构建基于ChatGPT的嵌入式(Embedding)本地医疗客服问答机器人模型(看完就会,看到最后有惊喜)1、GPT-4凌晨已发布赶紧申请,前端小白用最近用刚学的golang对接了GPT-3.5的6个接口再来说几个昨天的新品吧bing.com 推出了图片生成的AI,目前仅支持英文谷歌的bard AI也终于推出了,也是只支持英文,现在可以去申请了当然还有其他的,比如adobe公司等等吧太多了。ChatGPT能帮我们解决的问题已经够多了,这里我就不一一列举了,因为大家都知道,但是今早竟然发现这么一个神奇的功能。可以让它扮演linux服务器,搭建测试环境,那么学习linux这玩意简直太省事了,不过目前好像只有GPT-4模型才支持这种玩法。2、来看看怎么玩吧让ChatGPT来扮演一个linux服务器,然后我们通过指令来像平常一样操作它,或者指定特定指令{}来处理首先一个你要先告诉它一个大前提,然后再根据这个前提去操作就完美了。我希望你扮演 Linux 终端的角色。我会输入命令,你将回复终端应该显示什么。我希望你只在一个唯一的代码块中回复终端输出,不要写解释。除非我告诉你这样做,否则不要输入命令。当我需要用中文告诉你一些东西时,我会在花括号中放入文本,就像这样 { }。我的第一个命令是 pwd。接下来就开始实际操作一番下面尝试几个常用的指令下面我想升级一下node版本,并让它打印安装一下docker,并让它打印版本接下来我要将docker服务设置为开机启动,并开启docker服务,并且查看当前docker环境下是否存在镜像没有镜像那就来pull一个nginx,然后再通过命令查看运行一个容器,并来查看一下最后再来搭建一个k8s的集群3、总结好了,竟然玩不下去了,那么今天就先到这里吧。玩了这么多,真的越来越觉得它的强大,等待GPT-4真的到来,现在也只能算是GPT-4的一个开始,可能需要的算力实在太大,现在GPT-4模型都还没有完全的推出,只能是GPTPLUS的用户才能使用而且使用还是有限制的,从最开始的三个小时100的对话,再到后来三个小时50次回话,到最近几天的三个小时25次回话。而且对外开放的API接口也只有很少的一部分人在使用,好的继续期待它每一天的飞速变化吧。我的go代码仓库: github.com/aehyok/go-a…我的个人博客:vue.tuokecat.com/blog我的个人github:github.com/aehyok我的前端项目:pnpm + monorepo + qiankun + vue3 + vite3 + 工具库、组件库 + 工程化 + 自动化不断完善中,整体框架都有了 在线预览:vue.tuokecat.com github源码:github.com/aehyok/vue-…
1、前言3、惊奇,竟然可以在ChatGPT的GPT-4模型让它扮演Linux服务器 搭建K8s和docker环境2、从零开始构建基于ChatGPT的嵌入式(Embedding)本地医疗客服问答机器人模型(看完就会,看到最后有惊喜)1、GPT-4凌晨已发布赶紧申请,前端小白用最近用刚学的golang对接了GPT-3.5的6个接口代码全部开源,GitHub地址为: github.com/aehyok/go-o…前端完全也能搭建, 前端完全也能搭建, 前端完全也能搭建, 本文中我使用的是后端语言golang,来调用的所有外部接口,但它们均是restful api,所以如果你使用的是其他语言,那么是完全可以替换的,包括nodejs或者直接使用前端请求都是可以实现我的功能的。后面有机会会使用vue3来添加一个页面,现在主要通过postman或者apifox来调试接口,主要为了验证逻辑想法。接下来首先来看看embeddings到底是什么吧1.1、 官网的介绍是这样的可以被应用于以下几种情况- 搜索(根据查询字符串的相关性对结果进行排名) - 聚类(根据相似性对文本字符串进行分组) - 推荐(具有相关文本字符串的项目被推荐) - 异常检测(识别关联度小的异常值) - 多样性测量(对相似性分布进行分析) - 分类(文本串按其最相似的标签进行分类)本文将主要学习第一种情况:搜索,根据相关性进行排名。也可以理解成搜索完一定会有结果,但是相关性有可能很低,有可能需要用户加以甄别。出来的信息可能不是用户需要的答案。1.2、 而chatgpt是这样跟我说的1.3、最后我的一句话理解便是:它可以将文本转换为固定长度的连续向量。比如我下面使用的 text-embedding-ada-002模型输出的向量维度便是1536,这个在官网是有描述的,大家可以认真看看), 同时它是可以将任意的文本转换为向量。那么接下来我会根据我的思路把我整个的搭建流程和调试思路都展现出来,方便自己后面进行复习查阅,也方便可能需要的你。2、架构流程图介绍从上图可以比较清晰明了的知道大致要干什么了准备测试数据:测试数据可能很多一个很大的数组,慢慢通过调用ChatGPT接口进行转换数据,然后将转换后的向量数据存储到qdrant云数据库中,相当于本地数据了。根据查询返回结果:首先还是将要查询的字符串调用ChatGPT接口转换为向量数据,然后再将向量数据与向量数据库中的进行匹配相似度,匹配结束可以再通过GPT-3.5或者GPT-4的模型接口进行进一步的优化数据处理。接下来就根据如下步骤一步步进行搭建Qdrant云数据库的搭建准备测试数据并写入云数据库进行查询并返回结果3、Qdrant云数据库的搭建3.1、初识Qdrant说白一点就是为了存储我自己的测试数据,不过它的重点是存储向量数据。来到github上看了一下:github.com/qdrant/qdra… , 有点牛逼 而且是Rust写的。那就来试试玩玩呗。3.2、创建云数据库通过github可以直接到云官网: Vector Search Database | Qdrant Cloud可以看到能免费创建一个免费套餐,拿来做个测试还是非常方便的。针对图示的配置,可以永久免费使用,所以基本的测试是没问题了,可以好好的愉快玩耍。找到左侧菜单Clusters然后右侧点击 Create,输入一个cluster名称(是不是可以翻译为集群名称??)。创建后等待一会儿在进行初始化。点击上面的api-key 或者左侧 Access 都可以创建访问云数据库的链接和api-key。记得复制好哟,这个跟ChatGPT生成的API-Key一样,只能看到一次,所以要保存好。3.3、通过curl 接口访问Swagger UI (qdrant.tech) 这个就是官方提供给我们的Swagger。可视化 RESTful Web Api我是通过这个主要看接口以及接口参数,主要还是通过postman或者apifox等工具来测试接口,swagger这里好像没有配置api-key的地方?ok可以看到我之前创建的 collect 还在,其实这个时候本来是要创建一个collect集合(在关系型数据库中可以叫做table表)。4、写入测试数据4.1、准备测试数据注意:以上数据来源于ChatGPT,仅供参考和测试使用然而我想要的数据结构是json数组的,那么继续使用ChatGPT进行装逼可以发现准备这一组测试数据,有一点不费吹灰之力的感觉,真是太爽了。这里就是准备的json数组,总共13条简单的记录而已,主要是为了看一下效果[ { "title": "感冒", "text": "感冒是一种由病毒引起的呼吸道感染。典型症状包括喉咙痛、流鼻涕、咳嗽、打喷嚏、头痛和发热。" }, { "title": "流感", "text": "流感(Influenza)是一种由流感病毒引起的呼吸道感染。症状与感冒相似,但通常更严重,包括高热、寒战、喉咙痛、咳嗽、鼻塞、肌肉痛和乏力。" }, { "title": "肠胃炎", "text": "肠胃炎是胃和肠道的炎症,通常由病毒、细菌或寄生虫感染引起。症状包括腹泻、呕吐、腹痛、恶心、发热和脱水。" }, { "title": "常见皮肤病", "text": "如湿疹、皮炎、脓疱疮、疱疹等。症状可能包括红肿、瘙痒、干燥、脱皮和疼痛。" }, { "title": "头痛", "text": "头痛有许多原因,如压力、紧张、缺水、缺乏睡眠等。头痛可能表现为钝痛、搏动痛、集中在头的某个部位等。" }, { "title": "过敏", "text": "过敏是免疫系统对外来物质(过敏原)的异常反应。症状包括打喷嚏、流鼻涕、鼻塞、喉咙痛、眼睛痒、红肿和喘息。" }, { "title": "高血压", "text": "高血压是血压持续升高的病状。许多高血压患者没有明显症状,但可能会引发头痛、眩晕、心悸和呼吸困难。" }, { "title": "糖尿病", "text": "糖尿病是一种由于胰岛素分泌不足或细胞对胰岛素反应不良导致的血糖水平过高的疾病。症状包括频繁的小便、口渴、饥饿、疲劳、视力模糊、感染和伤口愈合缓慢。" }, { "title": "哮喘", "text": "哮喘是一种慢性呼吸道炎症疾病,表现为气道对刺激物的过度反应。症状包括喘息、呼吸困难、胸闷和咳嗽。" }, { "title": "背痛", "text": "背痛可能是由于肌肉拉伤、韧带损伤、关节炎、椎间盘问题等原因引起的。症状包括持续或间歇性的背部疼痛、僵硬和肌肉痉挛。" }, { "title": "关节炎", "text": "关节炎是关节炎症的一个通用术语,可能是由于多种原因引起的,如磨损性关节炎、类风湿性关节炎等。症状包括关节疼痛、肿胀、僵硬和活动受限。" }, { "title": "痔疮", "text": "痔疮是肛门或直肠血管的炎症或肿胀。症状包括肛门疼痛、瘙痒、肿胀、出血和可能的肛门突出物。" }, { "title": "眼疾", "text": "如干眼症、结膜炎和近视等。症状可能包括眼睛干燥、瘙痒、红肿、分泌物和视力模糊。" }]4.2、go代码将测试数据转换为向量数据这里暂时就要用到ChatGPT的接口了看官网接口请求主要就两个参数,一个就是model 选择模型,我这里使用的是text-embedding-ada-002,另外一个input 就是我们要转换的数据字符串了,好了直接上代码看看func GetEmbeddings(ctx *gin.Context) dto.ResponseResult { // 配置日志 data, _ := ctx.GetRawData() var parameters map[string]interface{} // 包装成json 数据 _ = json.Unmarshal(data, &parameters) input := parameters["input"].(string) // n := m["n"].(int) // size := m["size"].(string) var response = GetEmbeddingApi(input) var obj map[string]interface{} if err := json.Unmarshal(response, &obj); err != nil { panic(err) } fmt.Println("Body:", obj) return dto.SetResponseData(obj) } func GetEmbeddingApi(input string) []byte { // 定义请求参数 embeddingModel := EmbeddingModel{ Model: "text-embedding-ada-002", Input: input, } // 定义请求地址 url := utils.OpenAIUrl + `/v1/embeddings` // 将请求参数转换为json格式 bytes, err := json.Marshal(embeddingModel) if err != nil { fmt.Println("Error:", err) // return dto.SetResponseFailure("调用openai发生错误") } // 定义请求 req := fasthttp.AcquireRequest() defer fasthttp.ReleaseRequest(req) req.SetRequestURI(url) req.Header.SetMethod("POST") req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+utils.OpenAIAuthToken) req.SetBody(bytes) // 定义响应 resp := fasthttp.AcquireResponse() defer fasthttp.ReleaseResponse(resp) if err := fasthttp.Do(req, resp); err != nil { fmt.Println("Error:", err) // return dto.SetResponseFailure("调用openai发生错误") } fmt.Println("Status:", resp.StatusCode()) return resp.Body() }我在代码里添加了详细的注释,对照代码看一下应该还是比较好理解的。这里其实就是通过go语言调用restful 接口 https://api.openai.com/v1/embeddings 请求,因为下面查询的时候还需要将查询字符串转换为向量数据,所以我单独进行了封装可以在两个地方调用4.3、循环上述方法将预准备的json测试数据全部转换为向量数据// 解析请求参数 var jsonData []map[string]string if err := c.Bind(&jsonData); err != nil { return dto.SetResponseFailure("error") } if len(jsonData) == 0 { return dto.SetResponseFailure("json is empty") } // 数据向量化 points := make([]Point, 0) for _, v := range jsonData { // 获取文本内容 input := v["text"] // 获取文本内容的向量 response := GetEmbeddingApi(input) fmt.Println(response, "response----response") var embeddingResponse EmbeddingResponse json.Unmarshal(response, &embeddingResponse) points = append(points, Point{ ID: uuid.New().String(), Payload: v, Vector: embeddingResponse.Data[0].Embedding, }) }4.4、将上面准备好的向量数据数组全部写入向量数据库现在向量数据通过ChatGPT接口转换好了,现在就需要将向量数据写入到Qdrant云数据库中。 下面主要是调用了CreatePoints方法,同样可以看看pr := PointRequest{ Points: points, } //存储 err := CreatePoints(utils.QdrantCollectName, pr) if err != nil { // common.Logger.Error(err.Error()) // c.JSON(http.StatusOK, common.Error(err.Error())) // return return dto.SetResponseFailure("数据上传发生错误") } // c.JSON(http.StatusOK, common.Success(nil)) return dto.SetResponseSuccess("数据上传成功")其实相对来说我前面也写过的,就是来调用Restful api写入到云数据库。func CreatePoints(collectionName string, pointRequest PointRequest) (err error) { response := &CommonResponse{} var reqBytes []byte reqBytes, err = json.Marshal(pointRequest) if err != nil { return } body, err := middleware.Send(http.MethodPut, collectionApi+"/"+collectionName+pointsApi+"?wait=true", reqBytes) if err != nil { return } err = json.Unmarshal(body, &response) if err != nil { return } if response.Result == nil { return errors.New(response.Status.(map[string]interface{})["error"].(string)) } return }这里其实就是通过go语言调用restful 接口 https://ui.qdrant.tech/#/points/upsert_points (点击查看具体的接口详情) 请求现在测试数据有了,向量数据库也有了,上一小节将测试数据转换为了向量数据,这里上面刚刚又写好了向量数据写入云数据库的接口。那么写入数据的基本完成了。通过运行接口来调试一下吧5、开始查询数据准备好查询数据,先通过##3.2将字符串转换为向量数据(也就是为什么进行封装上面的方法的原因),然后通过向量数据去查询云数据库,去查询相似度了5.1、将查询字符串转换为向量数据那么这里就先准备一下查询云数据库的接口var message ChatMeMessage if err := c.Bind(&message); err != nil { // c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) // return } response := GetEmbeddingApi(message.Text) json.Unmarshal(response, &response) fmt.Println(response, "response----response") var embeddingResponse EmbeddingResponse json.Unmarshal(response, &embeddingResponse)这里其实就是通过go语言调用restful 接口 https://api.openai.com/v1/embeddings 请求这里注意一下,我理解的正常的话只要云数据库有数据,就会返回数据的,无非相似度低一些而已。5.2、根据向量来查询匹配相关性高的前三条记录准备查询参数数据,然后到Qdrant云数据库进行查询params := make(map[string]interface{}) params["exact"] = false params["hnsw_ef"] = 128 sr := PointSearchRequest{ Params: params, Vector: embeddingResponse.Data[0].Embedding, Limit: 3, WithPayload: true, } //查询相似的 res, err := SearchPoints(utils.QdrantCollectName, sr) if err != nil { // common.Logger.Error(err.Error()) // c.JSON(http.StatusOK, common.Error(err.Error())) // return }这里其实就是通过go语言调用restful接口 https://ui.qdrant.tech/#/points/search_points(点击查看具体的接口详情) 请求5.3、通过chatGPT对查询的相关性数据进行优化其实上面查询出来数据列出来就完事了,但是我上面也说了相关性的问题,那么这里我们可以通过ChatGPT对于查询返回的数据加工一下。//组装本地数据 localData := "" for i, v := range res { re := v.Payload.(map[string]interface{}) localData += "\n" localData += strconv.Itoa(i + 1) localData += "." localData += re["title"].(string) localData += ":" localData += re["text"].(string) } messages := make([]ChatCompletionMessage, 0) q := "使用以下段落来回答问题,如果段落内容与\"" + message.Text + "\"不相关就通过查询返回信息。" q += localData system := ChatCompletionMessage{ Role: "system", Content: "你是一个医院问诊客服机器人", } user := ChatCompletionMessage{ Role: "user", Content: q, } messages = append(messages, system) messages = append(messages, user) var chatResponse = GetChatCompletionsApi(messages) var obj map[string]interface{} if err := json.Unmarshal(chatResponse, &obj); err != nil { panic(err) } fmt.Println("Body:", obj) // 最后我通过一个方法进行统一返回参数处理 return dto.SetResponseData(obj)5.4、调试效果这是我通过GPT-3.5模型的接口调试其返回结果并不是非常理想。但是如果通过GPT-4.0就完全可以达到我想要的结果了当然了我这里演示的数据较少,仅用作演示效果,但是这种简单的问答模式加上最后GPT来润色优化有点好用了。而且还可以进行优化,比如问的问题是本地没有的,通过GPT回答后,可以进行操作,将当前问答回写到本地云数据库,这样下次再有类似的问答,就可以直接使用本地的数据了,这里仅仅提供一点点的我思考的逻辑,不一定是对的。6、总结这个对于我来说,理解起来还是蛮费劲的,主要是一开始没有抓到重点,其实现在把思路捋顺了,从应用的层面来看也就那么回事,当然了目前我的理解还是比较浅显的,有待机会进一步深入摸索,大数据训练模型。是不是可以考虑训练一个自己的AI虚拟人。当然还有另外一个Fine-Tunes 跟Embedding有没有关系,我得继续研究研究了,感觉上还是非常好玩的。再次声明本文所有代码都已上传github github.com/aehyok/go-o…本文主要参考:github.com/coderabbit2…, 也感谢大佬的及时回复解答我的疑惑。我的个人博客:vue.tuokecat.com/blog我的个人github:github.com/aehyok我的前端项目:pnpm + monorepo + qiankun + vue3 + vite3 + 工具库、组件库 + 工程化 + 自动化不断完善中,整体框架都有了 在线预览:vue.tuokecat.com github源码:github.com/aehyok/vue-…7、惊喜多多当然有可能很多巨佬比我知道的还早惊喜便是: Cursor | Build Fast一个目前免费使用GPT-4模型的生成代码,学习代码,修改代码,发现bug,处理bug的工具,可直接打开项目文件进行实操惊喜便是:www.steamship.com如果你想调用GPT-4的接口,但苦于申请还在等待列表,那么你可以尝试一下这个,注意目前也是免费哟惊喜便是:如果你想调用官方openai.com的接口,那么可以考虑一下这个第三方平台 api2d.com/r/186083,目前…
1、前言我的ChatGPT 系列文章3、惊奇,竟然可以在ChatGPT的GPT-4模型让它扮演Linux服务器 搭建K8s和docker环境2、从零开始构建基于ChatGPT的嵌入式(Embedding)本地医疗客服问答机器人模型(看完就会,看到最后有惊喜)1、GPT-4凌晨已发布赶紧申请,前端小白用最近用刚学的golang对接了GPT-3.5的6个接口GPT-4 API waitlist (openai.com) 有兴趣的赶紧申请一下吧。GPT-4更新最大的变化是可以输入并识别图片了,前两天写的这篇文章,算了,就这样赶紧发了 哈哈ChatGPT给人们日常生活带来的影响,尤其在软件科技圈,层出不穷的新品。或与十多年前的iPhone非常的相似,带来一轮变革或新陈代谢。必应日活最近也首次破亿,这一波微软真的是赢麻了。ChatGPT正式整合进Azure中,相信对于开发者来说是更大的利好。即将到来的GPT-4是多模态模型,可能同时理解图像、声音、文本和视频。也就是说,万物皆可作为输入和输出。这个真的是太刺激了。就看接下来发布之后,api的开放程度了,拭目以待啊。再来说一下,最近学习的GPT-3.5吧,由于种种原因,其实官网的功能也是足够的强大,只不过需要我们做到科学的上网才能使用。而在GPT-3.5之前开放的api,返回给我们的数据可以说是没什么参考价值吧。在3月1日发布的GPT-3.5之后,api接口的返回数据质量,也发生了质的提升,种种迹象表明,现在的GPT模型势必会给各行各业产生不小的冲击,引领我们被动的进行变革,也许不是现在,但绝对已经在路上了,因为更强大的GPT-4.0也马上蓄势待发,看着这个趋势,作为一个程序员的我,是怎么也安耐不住的,必须参与其中,哪怕只是进行了解学习,对其接口数据组织进行提前的预热学习吧。种种迹象表明,得好好学习一波了。2、对接openai接口的全过程可以按照这个顺序来尝试一下科学上网这个有很多就不说了openai官网注册账号也有非常的多,也不提了openai官网创建apikey本地开发可以使用代理进行开发了解go语言初级开发了解go语言的gin框架和fasthttp框架了解openai官网的api开干写代码,其实如果代码遇到了问题很大程度上你可以把代码发送给openai,大概率它可以帮你找到问题这里着重说一下代理的问题,通过本地调试的时候应该是没有办法访问接口 api.openai.comIce-Hazymoon/openai-scf-proxy: 使用腾讯云函数一分钟搭建 OpenAI 免翻墙代理 (github.com)geekr-dev/openai-proxy: 基于 Go 实现 OpenAI API 代理(适用于腾讯云云函数) (github.com)这里一种是nodejs实现的腾讯云函数,一种是go语言实现的云函数,这两种方式相对来说是最简单的,当然还可以有其他的方式了3、go语言初级开发由于年后时间相对充裕,所以在结合自身状况的前提下,我开启了go语言的学习,这期间真的好多的问题甚至是代码异常都是通过openai解决的, 也就是它能给予我莫大的帮助说句不太好听的话:最近掘金好像没怎么使用了,而且连签到都经常忘记了,百度和谷歌也就更不用说了,一定程度上,改变了我处理问题的方式。不知道掘金通过后台的访问数据,能否看出一点问题。所以最近我就通过go语言,算是原生的来调用了几个openai官网对外提供的接口。这里也有go语言开发的类库,当然其他语言的也都有openai类库 github.com/sashabarano… ,目前1766个starpython github.com/openai/open… 5863个starpython github.com/acheong08/C… 20837个starC# github.com/betalgo/ope… 1071个starnodejs github.com/transitive-… 10980个starnodejs github.com/openai/open… 1890个starjava github.com/TheoKanning… 1492个star各个语言都有,发展可谓百家争鸣,当然自家的python可谓有得天独厚的优势4、通过go对接openai下面我简单说一下我通过go是如何来对接openai接口的,分为以下几个小节4.1、gin框架Gin 是一个使用 Go 语言编写的 Web 框架,具有轻量、快速和易于使用的特点。Gin 框架提供了许多 Web 开发中常用的功能和中间件,包括路由、请求参数解析、日志记录、错误处理、认证授权、Swagger 文档生成等,使得开发者可以更加方便快捷地进行 Web 开发。已经使用强大的路由,对外对前端提供接口路由(路由可以分组,还可以继续分组真的很棒)可以使用Swagger文档提供与前端交流的媒介(生成接口预览、参数说明、还可调试、返回数据等等)将会使用中间件 验证token、日志记录、错误处理等swagger如下,目前开发的接口就这几个而已,后续会慢慢的增加业务,不断优化。这是我目前对接openai 的几个路由4.2、fasthttp在go中我主要是通过fasthttp类库来实现调用openai的接口的,没有使用上面说到的类库,主要是想学习一下通过fasthttp来调用接口相对于标准库提供的net/http包,Fasthttp具有更高的性能和更低的内存占用。它也支持HTTP/1.x和HTTP/2协议。同样fasthttp也有极其强大的其他功能:快速的路由匹配。Fasthttp使用高效的路由匹配算法来快速地匹配请求。支持中间件。Fasthttp支持中间件,可以让开发人员在处理请求之前或之后添加自定义逻辑,例如身份验证、日志记录等。支持文件服务器。Fasthttp可以快速地为静态文件提供服务,这对于处理大量静态内容的Web应用程序非常有用。容易学习和使用。Fasthttp采用简单的API设计,并提供了丰富的文档和示例,使开发人员可以快速学习和使用。4.3、代码实现看openai官网可以发现,这个接口主要的参数就是model和messages,而messages中又嵌套了一个数组结构体type ChatModel struct { Model string `json:"model"` Messages []Message `json:"messages"` } type Message struct { Role string `json:"role"` Content string `json:"content"` }所以这里我通过struct创建两个结构体,来组装参数,并通过后面的json:"model" 设置被转换为json后对应的字段接下来看看整个函数,其中我也添加了我对代码的理解注释,毕竟刚学习可能理解的不到位func GetChatCompletions(ctx *gin.Context) dto.ResponseResult { // 通过GetRawData获取前端传递的JSON数据结构 data, _ := ctx.GetRawData() // 将data数据 包装成json数据 var m map[string]interface{} _ = json.Unmarshal(data, &m) // 这里我定义的参数是content获取传入的参数 content := m["content"].(string) // 组装openai 接口的参数实体 chatModel := ChatModel{ Model: "gpt-3.5-turbo", Messages: []Message{ {Role: "user", Content: content}, }, } // 将实体结构转换为byte数组 bytes, err := json.Marshal(chatModel) fmt.Println(string(bytes), "bytes") if err != nil { fmt.Println("error:", err) return dto.SetResponseFailure("数据转换错误") } // openai接口地址,可通过代理处理 url := utils.OpenAIUrl + `/v1/chat/completions` // 定义fasthttp请求对象 req := fasthttp.AcquireRequest() // 使用defer关键字可以确保在函数返回之前,即使出现了错误,也会释放请求对象的内存,从而避免内存泄漏和浪费。 // 当请求处理完成时,应该调用fasthttp.ReleaseRequest(req)来将请求对象返回给对象池。 defer fasthttp.ReleaseRequest(req) // 设置请求的url地址,请求头,以及通过SetBody设置请求的参数 req.SetRequestURI(url) req.Header.SetMethod("POST") req.Header.Set("Content-Type", "application/json") // req.Header.Set("Content-Type", "application/octet-stream") req.Header.Set("Authorization", "Bearer "+utils.OpenAIAuthToken) //gpt-3.5-turbo-0301 req.SetBody(bytes) // 这里跟上面AcquireRequest 类似的,一个是请求对象,一个是返回对象 resp := fasthttp.AcquireResponse() defer fasthttp.ReleaseResponse(resp) // 通过fasthttp.Do真正的发起对象 if err := fasthttp.Do(req, resp); err != nil { fmt.Println("Error:", err) // return dto.SetResponseFailure("调用openai发生错误") ctx.JSON(200, gin.H{"data": "调用openai发生错误"}) } fmt.Println("Status:", resp.StatusCode()) // 将返回对象中的body数据转换为json数据 var obj map[string]interface{} if err := json.Unmarshal(resp.Body(), &obj); err != nil { panic(err) } fmt.Println("Body:", obj) // 最后我通过一个方法进行统一返回参数处理 return dto.SetResponseData(obj) }4.4、调试效果目前初步完成了五个接口,细节的参数可能在需要的时候还会添加,具体后端代码 github.com/aehyok/go-a…4.5、前端展示效果目前接口算是写好了,准备跟前端进行联调了,其实前端还有很多工作要做的,慢慢的把自己使用的这个工具好好优化。5、总结看似对接几个接口很简单,但是整个过程要学习的东西还是蛮多的,如果你也有兴趣学习,欢迎大家一起交流。我的go代码仓库: github.com/aehyok/go-a…我的个人博客:vue.tuokecat.com/blog我的个人github:github.com/aehyok我的前端项目:pnpm + monorepo + qiankun + vue3 + vite3 + 工具库、组件库 + 工程化 + 自动化不断完善中,整体框架都有了 在线预览:vue.tuokecat.com github源码:github.com/aehyok/vue-…
1、前言上一篇文章算的话是9月20日发的,到今天11月10日,大致有50天没有发文了。这段时间当然也在学习,一方面解决自己的问题,另外一方面公司刚好也比较忙,到现在应该说是又可以回归正常了。那么还说什么呢?开始卷文章学习吧。最近接触了一点公司的后端代码,刚好自己有非常多的疑问,带着这些疑问,我就来看看公司后端的代码是如何实现的。由于工作中表单的录入工作相对频繁,而且有非常多的下拉选项,所以就先来看看字典项目的获取。不看不知道,一看吓一跳,后端们用了史上最简单粗暴的方式进行处理的。也就是每个下拉都会调用mysqlconst GetDictionaryData = (typeCode) => { // 通过sql返回列表数据 select code, name from dictionary where typeCode = typeCode // result = 返回列表 return result } 然后前端怎么做呢?在用到字典的地方调用接口就完事了。对于一般的系统,用户量比较少,或者并发性能要求没那么高,慢点就慢点也就无所谓了。这其中其实存在蛮多的问题?来看看我是怎么处理的呢?不能说我处理的好吧,只能说在现有基础上做了一些改进,有效的提升了一部分性能和方便性。2、前后端的第一波调整2.1、后端这里其实我是将后端的字典项,存到了redis中,先简单看一下模拟方法const GetDictionaryData = (typeCode) => { // 先判断redis中是否缓存 if(redis.get(typeCode)) { return redis.get(typeCode) } else { // 先从数据库中获取 var list = mysql查询返回结果 redis.set(typeCode, list); return list; } }在后端简单将字典项目缓存到redis,这样有效的缓解了数据库的访问压力。不用每次都来调用数据库。2.2、前端前端的考虑跟后端有点类似,那就是缓存一下const GetDictionaryData = async (typeCode) => { // 比如这里先来判断localStorage或者sessionStorage等等中是否缓存 if(localStorage.getItem(typeCode)) { return JSON.parse(localStorage.getItem(typeCode)) } else { const result = await getApi({typeCode}) if(result.code === 200){ localStorage.setItem(typeCode, JSON.stringify(result.data)) return result.data } } }也就是在调用接口前,先判断浏览器缓存中是否存储了字典的数据项,如果有那么直接使用本地的,不用调用后端接口了,如果缓存不存在,则通过接口调用,并写入到本地缓存中。3、前后端第二波更新在第一波更新中存在一个小问题,如果后端数据更新了,后端的缓存数据也更新了,而我们前端还是拿的我们浏览器缓存的,那么数据就出现了非一致性的问题了。那现在我就来进行简单的调整。3.1、后端const GetDictionaryData = (typeCode,timeStamp) => { // 先判断redis中是否缓存 if(redis.get(typeCode)) { var result = redis.get(typeCode) if(result.timeStamp != timeStamp) { return redis.get(typeCode) } else { return { timeStamp } } } else { // 先从数据库中获取 var list = mysql查询返回结果 var result = { list: list, timeStamp: 时间戳 } redis.set(typeCode, result); return result; } }再来看一下前端代码的修改3.2、前端先说一下思路,因为害怕后端数据会更新,所以每次都进行调用接口来获取字典项目,但是多了一个参数,timeStamp,第一次的时候可以不传递。获取到接口返回的数据后还是要进行浏览器缓存存储。const GetDictionaryData = async (typeCode) => { // 比如这里先来判断localStorage或者sessionStorage等等中是否缓存 if(localStorage.getItem(typeCode)) { let cache = JSON.parse(localStorage.getItem(typeCode)) const result = await getApi({ typeCode: typeCode, timeStamp: cache.timeStamp }) if(result.code === 200 ) { // 如果本地时间戳与接口返回的时间戳一致 // 则后端只返回时间戳,字典列表项就不返回了 // 直接使用前端缓存的字典项 if(result.data.timeStamp === cache.timeStamp){ return cache.list } else { // 如果时间戳不一致,则代表后端接口数据返回可能发生变更了 // 先更新缓存 localStorage.setItem(typeCode, JSON.stringify(result.data)) return result.data.list } } } else { // 则代表缓存没有数据 const result = await getApi({typeCode}) if(result.code === 200 ) { { localStorage.setItem(typeCode, JSON.stringify(result.data)) return result.data.list } } }通过时间戳字段进行对比,时间戳相同,则代码钱后端数据一致,后端可以不传递list字典项目,只传递时间戳,方便与前端比对。前端传递的时间戳如果与后端的不一致,那后端就需要将字典项目list 和时间戳一起返回,前端需要更新浏览器缓存。这里虽然每次都请求了服务器,但当字典第一次从数据库获取被缓存之后,就相当于只返回时间戳字段,而且对于获取的数据是读取的是redis缓存中的,对mysql数据库服务器的压力将大大减少。当然redis的作用也绝不仅仅就是缓存,还有很多其他更牛逼的功能。4、可能还有第三波如果系统够大,做的更精细化一些。是不是针对字典项目会有专门的地方进行维护,维护到mysql数据库的时候,同时会同步到redis缓存中,这样后端的缓存将会使用的更加到位吧?也只是我的猜测,实际的话要根据具体的业务需求来吧5、总结最近在公司折腾了一点后端,初步想法是最近半年允许的话,在公司项目的基础上多搞一下后端,全身心投入两年前端,自己感触也颇多,等年底有空的时候也来唠一唠。搞一搞mysql数据库性能方面的优化搞一搞redis在后端中的角色成分搞一搞比如rabbitMQ 消息队列搞一搞微服务相关的搭建构件当然还有其他的,比如Grpc、DDD、IOC、AOP、Docker、K8S、Cron、JWT、 等等吧,加油趁着现在还有折腾的欲望再卷一卷。当然我搞的是比较偏门的语言:.net core,有兴趣的或者正在路上的咱们可以一起多交流。
大家好,我是 那个曾经的少年回来了。10年前我也曾经年轻过,如今已步入被淘汰的年龄,但如下幡然醒悟,所以活在当下,每天努力一点点,来看看2024年的时候自己会是什么样子吧,2024年的前端又会是什么样子,而2024年的中国乃至全球又会变成什么样子,如果你也有想法,那还不赶紧行动起来。期待是美好的,但是更重要的是要为美好而为之奋斗付诸于行动。本文所涉及到的所有源码已开源: github.com/aehyok/taro…1、安装// 全局安装脚手架,后面创建项目方便 npm i -g @tarojs/cli // 查看版本,确认安装是否成功 taro -v我目前的版本是3.5.4,前几天写文章的时候用的是3.5.3.2、创建项目taro init taro3-vue3-demopnpm i pnpm dev:weapp // 后面我把命令改为了start然后在微信开发者工具中打开dist会报如下错误查看到github官网github.com/NervJS/taro… 前几天使用的3.5.3版本,是有这个bug的,将版本降低到3.5.2 果然就可以了。这几天taro官网出了3.5.4版本,也是没问题了。3、taro3 vue3 echarts关于echarts在微信小程序中如何使用,官网这里有一个简单的说明 echarts.apache.org/handbook/zh…。 更多的详情可以查看github上的开源项目 github.com/ecomfe/echa…通过梳理和尝试,终于在微信小程序中可以使用echarts了。4、复制ec-vancas到项目中首先我把echarts-for-weixin开源项目下载了下来,然后把 ec-canvas 整个文件夹拷贝到项目的 src/components下, 目录如下所示5、当前页面引入在当前页面的index.config.ts文件中添加 usingComponents,引入echarts组件,也就是我们复制到components的组件module.exports = { navigationBarTextStyle: 'black', navigationBarTitleText: 'echarts地图示例', backgroundColor: '#eeeeee', backgroundTextStyle: 'light', usingComponents: { 'ec-canvas': '../../components/ec-canvas/ec-canvas' } }6、当前组件中使用<template> <view class="echarts"> <ec-canvas id="chart-dom-area" canvas-id="chart-area" :ec="ec"></ec-canvas> </view> </template> <script lang="ts" setup> import { reactive } from 'vue' import * as echarts from '@/components/ec-canvas/echarts' import './index.scss' function initChart(canvas: any, width: any, height: any) { // echarts.init初始化 const chart = echarts.init(canvas, null, { width, height }) canvas.setChart(chart) const option = { xAxis: { type: 'category', boundaryGap: false, data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] }, yAxis: { type: 'value' }, series: [ { data: [820, 932, 901, 934, 1290, 1330, 1320], type: 'line', areaStyle: {} } ] } chart.setOption(option) return chart } const ec = reactive({ onInit: initChart }) </script>里面最重要的就是绑定 ec.onInit,然后剩下的echarts配置跟官网就没有区别了。7、效果最终页面的展示效果如下8、注意事项config/dev.js开启 production,会对代码进行压缩env: { NODE_ENV: '"production"' },微信小程序中设置还可以通过微信小程序的分包来处理上传文件过大的问题9、总结有理由相信taro会越来越好吧,taro的体验性还是非常不错,接近使用vue3开发pc端和h5端,还能开发其他夸端的小程序,以及支持鸿蒙,不过我主要尝试的是微信小程序了。 本文所涉及到的所有源码已开源: github.com/aehyok/taro… 。我的个人博客:vue.tuokecat.com/blog我的个人github:github.com/aehyok我的前端项目:pnpm + monorepo + qiankun + vue3 + vite3 + 工具库、组件库 + 工程化 + 自动化不断完善中,整体框架都有了 在线预览:vue.tuokecat.com github源码:github.com/aehyok/vue-…
大家好,我是 那个曾经的少年回来了。10年前我也曾经年轻过,如今已步入被淘汰的年龄,但如下幡然醒悟,所以活在当下,每天努力一点点,来看看2024年的时候自己会是什么样子吧,2024年的前端又会是什么样子,而2024年的中国乃至全球又会变成什么样子,如果你也有想法,那还不赶紧行动起来。期待是美好的,但是更重要的是要为美好而为之奋斗付诸于行动。1、前言Tauri 是一个跨平台 GUI 框架,与 Electron 的思想基本类似。Tauri 的前端实现也是基于 Web 系列语言,Tauri 的后端使用 Rust。Tauri 可以创建体积更小、运行更快、更加安全的跨平台桌面应用。众所周知,Electron 相当于是打包了一个小型浏览器,体积大,还占内存。而 Tauri 开发的应用,前端使用操作系统的 webview,后端集成了 Rust,使得打包后的体积相当小。有人对比了打包同样一个 Hello World 程序,Electron 打包的应用在 50 MB 左右,而 Tauri 只有 4 MB 大小。-------这里摘自掘友的文章# 昆吾kw2、准备环境2.1、打开命令行执行xcode-select --install这个过程可能需要几分钟的时间2.2、安装rustcurl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh2.3、window下安装Microsoft Visual Studio C++ 构建工具下载链接:visualstudio.microsoft.com/zh-hans/vs/VS2022 Enterprise 企业版安装时一定要选择C++tauri.app/zh/v1/guide…下载webview2下载链接:developer.microsoft.com/zh-cn/micro…下载完毕后安装即可下载Rust 下载链接: www.rust-lang.org/tools/insta…下载完毕后安装即可2.4、mac下安装可以去官网看一下写的很清楚3、创建tauri桌面项目3.1、命令行安装pnpm create tauri-app使用vscode打开项目查看pnpm dev // 打开的是前端vue项目 pnpm tauri dev // 打开客户端如下 复制代码总共大概使用了6分钟第二次运行的时间就大大缩短3.2、打包编译首先找到src-tauri\tauri.conf.json,修改如下节点"identifier": "com.aehyok.dev",然后开始编译pnpm tauri build之后发现错误,我翻了墙然后重新编译就OK了,因为要下载github上的压缩包3.3、 本地安装客户端生成了msi安装包之后,直接点击本地安装,当然是window版本的4、rust入门其实上面已经安装好了rust环境。我们前端使用的包管理工具是npm,而rust的包管理程序是cargo, 直接在命令行中输入 cargo。本文主要先来个入门4.1、开始创建rust项目cargo new rust-demo执行完毕后可以看到如下所示的文件目录或者可以像npm一样 npm init,来使用cargo init 也是一样可以创建项目。创建完项目直接run便可以看到hello world,证明项目跑起来了。cargo run 4.2、依赖处理crates.io 官网 有点类似于 www.npmjs.com 从上面可以搜索到任何想使用的包,如果有的话。如果没有你可以实现,然后在这里发布包。比如我想找 reqwest当然也可以通过命令进行查找cargo search reqwest官网看一下 reqwest将第一段两个依赖拷贝到Cargo.toml文件中,如下图reqwest = { version = "0.11", features = ["json"] } tokio = { version = "1", features = ["full"] }reqwest 是一个简单而强大的 RUST HTTP 客户端,用于浏览器异步 HTTP 请求。支持 xmlHttpRequest, JSONP, CORS, 和 CommonJS 约束。reqwest 的 async 使用的是 Tokio 的,所以要同时加入 Tokio 的依赖。tokio 目前是最受欢迎的异步运行时,功能强大,还提供了异步所需的各种工具(例如 tracing )、网络协议框架(例如 HTTP,gRPC )等等。4.3、调用依赖然后将第二个代码拷贝到 src/main.rs中use std::collections::HashMap; #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { let resp = reqwest::get("https://httpbin.org/ip") .await? .json::<HashMap<String, String>>() .await?; println!("{:#?}", resp); Ok(()) }4.4、执行开始运行项目cargo run可以发现最后打印出结果了5、总结Mozilla 创造了 Rust,Facebook、Apple、Amazon、Microsoft 和 Google 都使用 Rust 去开发系统基础设施、加密、虚拟化以及其他的层级较低的软件。掘友有一篇翻译 RUST 是 JavaScript 基建的未来,有理由相信慢慢的会有越来越多的人来拥抱rust。我的个人博客:vue.tuokecat.com/blog我的个人github:github.com/aehyok我的前端项目:pnpm + monorepo + qiankun + vue3 + vite3 + 工具库、组件库 + 工程化 + 自动化不断完善中,整体框架都有了 在线预览:vue.tuokecat.com github源码:github.com/aehyok/vue-…
大家好,我是 那个曾经的少年回来了。10年前我也曾经年轻过,如今已步入被淘汰的年龄,但如下幡然醒悟,所以活在当下,每天努力一点点,来看看2024年的时候自己会是什么样子吧,2024年的前端又会是什么样子,而2024年的中国乃至全球又会变成什么样子,如果你也有想法,那还不赶紧行动起来。期待是美好的,但是更重要的是要为美好而为之奋斗付诸于行动。1、前言在上一篇中,通过一些依赖,最终调用 git cz 来替代 git commit ,并通过 npm scripts 并最终达到规范提交的message。具体操作的源码项目 github.com/aehyok/taro…那么本篇就来看看git commit都可以做哪些校验呢?eslint 校验代码是否符合代码规范prettier 格式化代码拦截校验 git commit 提交的内容其实还可以执行测试用例等等2、如何在提交commit做一些校验2.1、commitlint安装pnpm i @commitlint/cli @commitlint/config-conventional -D 安装完后,在项目根目录添加commitlint.config.js,并在文件中添加如下配置module.exports = { extends: ['@commitlint/config-conventional'] }这里提一下其实通过查看commitlint官网可以发现,它的配置方式有好几种的,这里我使用的是第二种,如下图所示,当然你也可以去尝试其他的方式,比如第一种使用也非常的广泛,这里需要承接我的上一篇,开头文章有提到,有需要的可以点击查看。2.2、husky git钩子husky 是一个可以配置 git 钩子变得更简单的工具。支持所有的git钩子。好像大致有30个左右的钩子,有兴趣的可以去查查,这里我主要使用了两个钩子。pnpm i husky -D在package.json中添加"hu": "husky install"并在项目根目录的命令行中执行 pnpm hu,执行完毕之后可以在项目中观察到多了一个文件夹 .husky,两个钩子函数, commit-msg 和 pre-commit。commit-msg#!/bin/sh . "$(dirname "$0")/_/husky.sh" npx --no-install commitlint --edit $1pre-commit#!/bin/sh . "$(dirname "$0")/_/husky.sh"2.3、lint-stagedpnpm i lint-staged -D npx lint-staged在package.json中添加"lint-staged": { "*.{js,ts,vue}": [ "eslint --ext .js,.ts,.vue" ], "*.{js,ts,md,html,scss}": "prettier --write" },2.4、prettier 、eslintprettier和eslint相信很多前端都知道这是干什么的,这里我就不进行赘述了,有需要的直接去查一查。这里我列出我在项目中需要安装的其他一些依赖项"eslint-config-airbnb-base": "^15.0.0", "eslint-config-prettier": "^8.3.0", "eslint-config-taro": "3.3.16", "eslint-plugin-import": "^2.25.3", "eslint-plugin-prettier": "^4.0.0",2.5、prettier 配置如下.prettierrc.jsmodule.exports = { printWidth: 100, tabWidth: 2, useTabs: false, semi: false, // 未尾逗号 vueIndentScriptAndStyle: true, singleQuote: true, // 单引号 quoteProps: 'as-needed', bracketSpacing: true, trailingComma: 'none', // 未尾分号 jsxBracketSameLine: false, jsxSingleQuote: false, arrowParens: 'always', insertPragma: false, requirePragma: false, proseWrap: 'never', htmlWhitespaceSensitivity: 'strict', endOfLine: 'lf' }2.6、eslint配置.eslintrc.jsmodule.exports = { env: { browser: true, es2021: true, node: true }, extends: ['plugin:vue/essential', 'airbnb-base', 'plugin:prettier/recommended'], parserOptions: { ecmaVersion: 13, parser: '@typescript-eslint/parser', sourceType: 'module' }, plugins: ['vue', '@typescript-eslint'], rules: { semi: 'off', // 结尾分号设置 'comma-dangle': 'off', // 尾随一个多余的逗号 'vue/no-multiple-template-root': 'off', 'vue/require-v-for-key': 'off', 'no-console': 'off', // console。log可用 'vue/multi-word-component-names': 'off', 'import/no-unresolved': 'off', 'import/extensions': 'off', 'vue/no-v-model-argument': 'off', 'vue/no-v-for-template-key': 'off', 'no-eval': 'off', 'import/prefer-default-export': 'off', 'no-undef': 'off', 'consistent-return': 'off', 'no-param-reassign': 'off', 'vue/no-mutating-props': 'off', 'import/no-extraneous-dependencies': 'off', 'no-plusplus': 'off' // i++可用 }, settings: {} }当然还有其他的3 实际校验3.1、直接命令行git commit这个时候可以发现通过commitlint进行校验,发现我们的commit内容是不规范的,间接的对git commit 进行了规范的校验。3.2、一个命令一条龙// package.json中的scripts添加 "git": "git add . && git cz && git push", //然后执行pnpm git选择代码修改类型输入commit提交内容再进行eslint代码规范校验再通过prettier自动格式化代码如果都校验成功,则开始真正的执行git commit -m 最后一步 git push开头也说了其实还可以进行很多的事情,比如执行 测试用例 等4、总结这一篇其实我讲解了对git commit 提交内容的规范方式 juejin.cn/post/713448… , 紧接着本篇对git commit 提交时可以做的一些校验。如果使用的是github,刚好有发布版本的需求,还可以参考我使用的release-it进行自动化的发版juejin.cn/post/713262… .不断的折腾我才发现自己的知识面是多么的狭小,越学习感觉要补充的知识就越多,那就不断的来优化自己的知识体系吧。本文折腾的源码: github.com/aehyok/taro…我的个人博客:vue.tuokecat.com/blog我的个人github:github.com/aehyok我的前端项目:pnpm + monorepo + qiankun + vue3 + vite3 + 工具库、组件库 + 工程化 + 自动化不断完善中,整体框架都有了 在线预览:vue.tuokecat.com github源码:github.com/aehyok/vue-…
前言:大家好,我是 那个曾经的少年回来了。10年前我也曾经年轻过,如今已步入被淘汰的年龄,但如下幡然醒悟,所以活在当下,每天努力一点点,来看看2024年的时候自己会是什么样子吧,2024年的前端又会是什么样子,而2024年的中国乃至全球又会变成什么样子,如果你也有想法,那还不赶紧行动起来。期待是美好的,但是更重要的是要为美好而为之奋斗付诸于行动。直接封面图可以发现:最左侧的便是客户机,中间的就是负载均衡的机器,最右侧的便是通过负载后真正访问的服务器,我这里只画了两台服务器,实际情况根据需求可能还会不断增加。1、前端负载均衡实现先上一个前端版本,同一份打包编译后的文件,分别部署到one和two.因为我只有一台服务器,所以这里通过端口进行演示: one对应端口91,two对应端口92.通过upstream nginxlbdemo进行负载配置.最后通过proxy_pass反向代理 upstream.如果有修改nginx的配置文件,修改完毕后记得 nginx -s reloadnginx-lb.conf可以放到/etc/nginx/conf.d路径下upstream nginxlbdemo{ server 127.0.0.1:91; server 127.0.0.1:92; } server { listen 90; location / { proxy_pass http://nginxlbdemo; } } server { listen 91; root /usr/local/aehyok/nginx-lb/one; index index.html index.htm; if ($request_method = 'OPTIONS') { return 204; } location / { try_files $uri $uri/ = 404; } } server { listen 92; root /usr/local/aehyok/nginx-lb/two; index index.html index.htm; if ($request_method = 'OPTIONS') { return 204; } location / { try_files $uri $uri/ = 404; } }部署完毕后,通过电脑查看,我没办法做测试,只能通过停掉91或者停掉92来访问90,都是没问题的。 那这里只好通过node开启后端服务来测试一下。2、node后端负载均衡下面的代码算算是模拟91和92两个端口,相当于两台服务器,拷贝到linux服务器上,通过 node index.js运行起来const http = require("http"); const port1 = 91 const server1 = http .createServer(function (req, res) { console.log(`Request: ${req.url}--port ${port1}`); res.end(`hello world! - ${port1}`); }) .listen(port1, "127.0.0.1"); server1.once("listening", () => { console.log(`Server http://127.0.0.1:${port1}`); }); const port2 = 92 const server2 = http .createServer(function (req, res) { console.log(`Request: ${req.url}--port ${port2}`); res.end(`hello world! - ${port2}`); }) .listen(port2, "127.0.0.1"); server2.once("listening", () => { console.log(`Server http://127.0.0.1:${port2}`); });然后重新配置nginx-lb.conf,然后 nginx -s reloadupstream nginxlbdemo{ server 127.0.0.1:91; server 127.0.0.1:92; } server { listen 90; location / { proxy_pass http://nginxlbdemo; } }然后通过 http://139.159.245.209:90 ,你可以不断的刷新,就可以看到node 后台的请求日志 91和92端口在不停的切换3、负载均衡的分类3.1、默认轮询nginx默认就是轮询,其权重默认都是1,服务器请求的顺序:第1个请求 91端口第2个请求 92端口第3个请求 91端口第4个请求 92端口第5个请求 91端口第6个请求 92端口第7个请求 91端口第8个请求 92端口 ....upstream nginxlbdemo{ server 127.0.0.1:91; server 127.0.0.1:92; }3.2、 加权轮询nginx根据配置的权重大小而分发给对应服务器对应权重 数量的其你,其权重默认都是1,服务器请求的顺序:第1个请求 91端口第2个请求 92端口第3个请求 92端口第4个请求 91端口第5个请求 92端口第6个请求 92端口第7个请求 91端口第8个请求 92端口第9个请求 92端口upstream nginxlbdemo{ server 127.0.0.1:91 weight=1; server 127.0.0.1:92 weight=2; }3.3、 ip_hashnginx 会让相同的客户端ip请求相同的服务器。upstream nginxlbdemo{ ip_hash; server 127.0.0.1:91; server 127.0.0.1:92; }3.4、 热备开始时负载均衡90只会访问91端口,如果91端口服务器发生故障,才会开启第二台服务器给提供服务。upstream nginxlbdemo{ ip_hash; server 127.0.0.1:91; server 127.0.0.1:92 backup; }4、总结nginx在后端非常的常见,但对于一向爱折腾的我有必要去学习一下。了解完以后。便可以发现,灰度发布其实就可以通过加权轮询来处理,当然nginx的体系还是非常庞大的继续学习吧。我的个人博客:vue.tuokecat.com/blog我的个人github:github.com/aehyok我的前端项目:pnpm + monorepo + qiankun + vue3 + vite3 + 工具库、组件库 + 工程化 + 自动化不断完善中,整体框架都有了 在线预览:vue.tuokecat.com github源码:github.com/aehyok/vue-…
大家好,我是 那个曾经的少年回来了。10年前我也曾经年轻过,如今已步入被淘汰的年龄,但如下幡然醒悟,所以活在当下,每天努力一点点,来看看2024年的时候自己会是什么样子吧,2024年的前端又会是什么样子,而2024年的中国乃至全球又会变成什么样子,如果你也有想法,那还不赶紧行动起来。期待是美好的,但是更重要的是要为美好而为之奋斗,付诸于行动。上一篇文章自己尝试了,一步一步的部署到linux的nginx上,一鼓作气再来尝试一下如何将自己的前端项目部署到docker。上一篇文章链接:juejin.cn/post/713337…1、理解一下dockerdocker 中的几个概念Repository: 仓库就是是集中存放镜像文件的场所,有点类似于github。Image:镜像,用来创建容器。Container: 容器,是一个可运行的镜像实例。Dockerfile: 镜像构建的模版,描述镜像构建的步骤。再来形象的理解一下dockerDocker可以帮助我们来构建和部署容器,我们只需要将自己的应用程序或者服务打包放进容器即可。容器是基于镜像启动起来的,容器中可以运行一个或多个进程。可以理解成镜像是 Docker 生命周期中的构建或者打包阶段,而容器则是启动或者执行阶段。容器基于镜像启动,一旦容器启动完成后,我们就可以登录到容器中安装自己需要的软件或者服务。2、linux上安装docker//安装docker sudo zypper in docker (yum) //查看docker版本 docker -v3、设置docker开机启动// 配置让docker服务岁系统自动启动 systemctl enable docker.service // 启动docker守护进程 systemctl start docker.service // 取消开机自动启动 systemctl disable docker.service // 停止docker服务 systemctl stop docker.service // 查看nginx服务的状态 systemctl status docker.service // 重启docker服务 systemctl restart docker.service4、修改docker镜像源vim /etc/docker/daemon.json // 如果权限不够可以要加上sudo sudo vim /etc/docker/daemon.json然后再daemon.json中添加"registry-mirrors" : [ "https://registry.docker-cn.com", "https://docker.mirrors.ustc.edu.cn", "http://hub-mirror.c.163.com", "https://cr.console.aliyun.com" ]5、拉取镜像//查看下载的镜像 docker images // docker中拉去nginx镜像 docker pull nginx // 查看nginx镜像 (IMAGE ID) docker images nginx6、启动一个容器//测试nginx镜像是否可用 docker run -d --name mynginx -p 80:80 605c-d 指定容器以守护进程方式在后台运行--name 指定容器名称,此处我指定的是mynginx-p 指定主机与容器内部的端口号映射关系,格式 -p [宿主机端口号]:[容器内部端口],此处我使用了主机80端口,映射容器80端口605c 是nginx的镜像IMAGE ID前4位执行上面的docker run后生成一串字符串(容器ID)说明执行成功ecs-78441:/etc/docker # docker run -d --name mynginx -p 80:80 605c ddc72b6047dba505f24559d181c99043be50eaa6a383549b7c0fe147266fb0b2测试nginx镜像通过浏览器进行访问(端口要记得做映射)上面我设置的端口是807、开始部署自己的项目7.1、开始将自己的vue前端项目打包到dist目录pnpm build7.2、在本地创建一个DockerfileFROM nginx:latest # 将项目根目录下dist文件夹下的所有文件复制到镜像中 /usr/share/nginx/html/ 目录下 COPY dist/ /usr/share/nginx/html/ COPY default.conf /etc/nginx/conf.d/default.conf7.3、同样在本地创建一个default.conf(nginx配置文件)server { listen 80; root /usr/share/nginx/html; index index.html index.htm; if ($request_method = 'OPTIONS') { return 204; } location / { try_files $uri $uri/ = 404; } }7.4、将打包后的文件dist文件夹和Dockerfile以及default.conf上传到linux服务器我这里是通过filezilla上传和查看的7.5、在服务器上cd到指定目录这个目录便是我上传dist和Dockerfile以及default.conf的目录,在命令行中执行cd /usr/local/aehyok/docker7.6、开始通过Dockerfile创建镜像(admin指的是镜像的名字)在这个目录直接运行以下命令,就开始根据Dockerfile文件开始构建镜像docker build -t admin . // 应该会提示Successfully tagged admin:latest // 说明创建成功了7.7、查看刚刚创建的镜像(找到admin镜像的IMAGE ID的前四位)docker images 7.8、通过镜像创建一个新的容器并运行容器docker run -d -p 80:80 --name myreact ba047.9、查看容器//列出容器 docker ps // 列出所有容器(包括未运行的容器) docker ps -a // 开启容器 (container id) docker start dsfsdfas docker stop dfdssdf7.10、通过浏览器进行访问地址即可,完美结束curl http://xx.xxx.xxx:80 如果可以查看到下载的html便可以直接通过浏览器进行访问8、总结不用靠后端也可以自己部署自己的项目,自己实际操作下来也没什么难度,加油继续折腾吧。我的个人博客:vue.tuokecat.com/blog我的个人github:github.com/aehyok我的前端项目:pnpm + monorepo + qiankun + vue3 + vite3 + 工具库、组件库 + 工程化 + 自动化不断完善中,整体框架都有了 在线预览:vue.tuokecat.com github源码:github.com/aehyok/vue-…
前言:大家好,我是 那个曾经的少年回来了。10年前我也曾经年轻过,如今已步入被淘汰的年龄,但如下幡然醒悟,所以活在当下,每天努力一点点,来看看2024年的时候自己会是什么样子吧,2024年的前端又会是什么样子,而2024年的中国乃至全球又会变成什么样子,如果你也有想法,那还不赶紧行动起来。期待是美好的,但是更重要的是要为美好而为之奋斗付诸于行动。本文主要讲解如何一步一步的来部署自己的前端项目,到linux服务器上。我自己的linux服务器选择的是opensuse。这里说明一下:opensuse 通过zypper可以方便的进行软件管理,类似centos的yum 软件。1、安装nginx// 查找nginx zypper search nginx // 通过zypper安装nginx zypper install nginx // 查看nginx是否安装成功,安装成功则出现版本相关信息 nginx -v2、设置nginx服务自动启动// 设置nginx开机自动启动 systemctl enable nginx // 取消开机自动启动 systemctl disable nginx // 启动nginx服务 systemctl start nginx // 停止nginx服务 systemctl stop nginx // 查看nginx服务的状态 systemctl status nginx //修改nginx配置文件等,重新载入 nginx -s reload3、配置nginx的配置文件在/etc/nginx/conf.d文件夹下新建一个html.confserver { ## 端口 listen 88; ## 前端项目打包后的路径 root /usr/local/qiankun/blog; index index.html index.htm; ## 设置跨域,其实可以忽略 add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS'; add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization'; if ($request_method = 'OPTIONS') { return 204; } location / { try_files $uri $uri/ = 404; } }4、创建项目这里我直接使用我自己的项目,这个项目也在github上开源了,谁都可以看到 github.com/aehyok/blog当然这里面已经包含了我的半自动化脚本和全自动 github Actions yml脚本,推送代码到指定分支的时候就会自动部署。这里要使用的便是我自己写的半自动化脚本。5、通过脚本执行打包后,自动拷贝到linux服务器在package.json中引入zxnpm i zx然后在package.json中的scripts"buildtome": "npm run build && zx ./scripts/prebuild.mjs",prebuild.js文件内容如下import { $, argv } from "zx"; export const isWin = () => { // win32 代表window平台 // darwin 代表mac平台 // return process.platform === "win32" ? true : false; } const root = process.cwd() let dir = isWin() ? '/h/github/blog': root console.log(process.platform, 'root') let path = `${dir}/docs/.vitepress/dist` const gitPullInfo = await $`scp -r ${path}/* root@81.69.241.99:/usr/local/qiankun/blog/` // const gitPullInfo = await $`cd ${path};`; if(gitPullInfo.exitCode === 0) { console.log('复制到linux远程服务器成功') } else { console.log('发生错误') }先来判断是在window执行的脚本,还是在mac下(linux应该也可以不过我没尝试过)两者获取路径的方式是不太一样。 然后其实就是通过scp指令,将本地打包编译好的目录,拷贝到服务器对应的目录上。关于免密登录这里我前两天整理过,如果有需要可以看看 juejin.cn/post/713155…最后访问网页6、总结不用靠后端也可以自己部署自己的项目,自己实际操作下来也没什么难度,加油继续折腾吧。我的个人博客:vue.tuokecat.com/blog我的个人github:github.com/aehyok我的前端项目:pnpm + monorepo + qiankun + vue3 + vite3 + 工具库、组件库 + 工程化 + 自动化不断完善中,整体框架都有了 在线预览:vue.tuokecat.com github源码:github.com/aehyok/vue-…
大家好,我是 那个曾经的少年回来了。10年前我也曾经年轻过,如今已步入被淘汰的年龄,但如下幡然醒悟,所以活在当下,每天努力一点点,来看看2024年的时候自己会是什么样子吧,2024年的前端又会是什么样子,而2024年的中国乃至全球又会变成什么样子,如果你也有想法,那还不赶紧行动起来。期待是美好的,但是更重要的是要为美好而为之奋斗付诸于行动。关于Inode详细解释我昨天整理了一点,有兴趣的可以了解一下:juejin.cn/post/713172…1、硬链接与软链接的定义硬链接(hard link)对一个文件进行修改,可能会影响到其他文件的内容,但是删除一个文件名,并不会影响其他文件名的访问。软链接又可以说成是符号连接,它有点类似于Windows的快捷方式,实际上它是一个特殊的文件。在符号连接中,文件实际上是一个文本文件,其中包含的有另一文件的位置信息。以下操作:我都是在linux系统上进行操作的。2、创建一个文件,并写入字符串touch aehyok echo "hello world"> aehyok cat aehyok通过截图可以发现,文件生成成功,内容字符串也写入了。3、创建硬链接创建一个指向aehyok文件的 硬链接hard-aehyokln aehyok aehyok-hard cat aehyok-hard ls -li两个文件除了名字不一样,其他都是一样的4、修改文件内容echo "hard hello world" >>aehyok-hard cat aehyok-hard cat aehyok修改硬链接文件中的内容之后,源文件中的内容也发生了变更echo "aehyok hello world">> aehyok cat aehyok cat aehyok-hard修改源文件之后,硬链接中的文件内容也同时发生了变更原文件与硬链接文件互为硬链接可以通过 stat xxx 命令来查看文件的inode信息可以发现Inode为2502924,他的硬连接数为2。你也可以发现Inode信息中不包含文件名。对于系统来说,文件名只是inode号码便于识别的别称或者绰号。5、创建软链接ln -s aehyok aehyok-soft cat aehyok-soft ls -li可以发现软链接文件属性跟源文件和硬链接属性有区别的。而且有一个明显的指向,指向了原文件aehyok。6、删除原文件删除原文件之后,硬链接文件可以正常访问,但是访问软链接文件报错了。这里与我们开头总结的定义正好吻合了。7、写入数据到软链接echo "delete hello world" >> aehyok-soft cat aehyok-soft cat aehyok ls -li可以发现,写入软链接数据之后,原文件竟然出现了,但是里面的数据块inode是新的了,因为软链接文件中保存的是有引用原文件路径的,但是原文件已经不存在了。现在相当于根据路径重新生成并写入原文件。8、总结硬链接不会创建额外 inode,它和源文件共用同一个 inode软链接会创建新的文件和 inode,但是软链接文件inode指向源文件的 inode建立硬链接时,源必须存在且只能是文件建立软链接时,源可以不存在而且可以是目录删除源文件不会影响硬链接文件的访问(因为inode还在)删除源文件会影响软链接文件的访问(因为指向的inode已经不存在了)
前言: 平常自己喜欢折腾,所以自己买了云服务器,有时候会捯饬一下,所以避免不了要跟linux系统打交道,那么第一步便是如何远程,如何开机如何重启的问题远程登录linux系统的三种1、直接命令行ssh加用户名和密码找个命令行窗口Window下的: CMD,Window PowerShell等等Mac下的终端以及VSCode下的终端都是可以的ssh root@139.159.245.209输入完命令后,再输入密码就登录到linux服务器上了这里当然有一种方式可以免密登录,需要在本地生成SSH密钥,然后拷贝到服务器上就可以实现cat ~/.ssh/id_rsa.pub | ssh root@139.159.245.209 “cat - >> ~/.ssh/authorized_keys”2、直接命令行ssh加密钥生成SSH密钥// 查看ssh版本,同时判断ssh是否已经安装 ssh -V // 生成SSH密钥 ssh-keygen -o // 导航到指定目录 cd ~/.ssh/ // 查看ssh,id_rsa.pub则为公钥,id_rsa则为私钥,将公钥复制到linux服务器(其实是文件里面的字符串) ls 拷贝到 ~/.ssh/authorized_keys,最终达到如下效果,输入命令直接进入,无需输入密码了,其实是直接验证密钥了3、xshell工具登录linux系统xshell下载地址:www.xshellcn.com/xiazai.html装之后,打开xshell,左上角新增会话会提示输入用户名以及密码登录成功会提示以下提示通过xshell进行记录,这样以后也无需登录。以上就是我平常登录linux系统的三种方式。4、关机、重启了解4.1、shutdown 方式// 关机 shutdown -h now // 重启 shutdown -r now4.2、init//关机 init 0 //重启 init 64.3、reboot//重启 reboot //强制重启 reboot -f //关机 reboot -p当然还有其他的方式,这里我常用这几种应该足够了5、总结很多时候真的就是熟能生巧,用的多了自然就记下来了,总结下来方便日后的使用。作为一个前端有时候可能会发布一下自己的前端项目,或者尝试一下CICD,再或者尝试写几个后端接口,那么多多稍稍都要跟linux打交道,越早的了解提升自己的技能箱,更好的拓展自我。
前言: 准备工作,可能需要一台linux服务器,或者直接在mac上进行安装,或者window上的docker使用。我这里的话自己有一台云服务器,所以还挺方便的。linxu服务器测试npm包的源代码:github.com/aehyok/ak-c…1、nexus3安装1.1、github开源地址github.com/sonatype/do…1.2、linux上安装1.2.1、查看docker镜像docker search nexus通过在linux上执行search 便可以查看到可以安装的镜像1.2.2、拉取docker镜像docker pull sonatype/nexus3找到上面STARS最多的,应该也就是使用最多的进行拉取镜像1.2.3、创建并运行容器docker run -d -p 8081:8081 --name nexus sonatype/nexus3创建并运行docker容器,如果端口和防火墙已经设置,便可以通过如下命令进行测试curl http://127.0.0.1:8081如果能看到html文件则说明部署成功了,同样便可以通过外网访问了2、nexus3网站配置2.1、 登录登录时使用默认用户admin,密码不知道就需要找默认的,点击Sign in时会提示你路径,这里我是这样查的,在linux服务器上输入以下命令find / -name 'admin.password' vim /var/lib/docker/volumes/8390ba0696e5fb161bc65f779dab7a192fc0e807929ea59f49805bedec2157b9/_data/admin.password直接将vim后 里面的字符串进行复制,复制当做密码就可以登录了,登录后进行修改密码2.2、创建Blob Stores输入类型下拉选择类型 【File】,然后输入【name】,name就是一个字符串而已,最后Save即可。2.3、创建npm Repositories点击[Create repository],以此可以选择npm的三个2.3.1、创建 hosted,先选择[npm(hosted)]这里的name是我自己写入的一个字符串 Blob store则是我们上面添加的npm,下面的直接选择默认的,最后点击Create repository.2.3.2、 创建proxy,先选择[npm(proxy)]Name: 输入一个字符串就可以 Remote storage: registry.npmjs.orgMaximum component age: 设置-1 Blog store: 选择之前添加的 npm 主要就这四个设置,设置完毕之后就点击最下方的[Create repository]2.3.3、 创建group,先选择[npm(group)]Name:输入一个字符串就可以 Blob store: 选择之前添加的npm Member repositories: 将添加的hosted和proxy都添加到右侧 主要是这三个设置,然后Create repository2.4、 创建用户输入如图所示的信息,添加权限角色,最后Create local user2.5、开启token权限认证从左侧菜单中点击[ReaIms],然后从[Active realms]中选择[npm Bearer Token ReaIm]到右侧后,保存即可。3、本地添加npm源// 将私有npm仓库添加到本地,这样npm安装的时候如果有使用到其他npm包,会先 nrm add anpm http://139.159.245.209:8081/repository/npm-group/ // 设置anpm为当前仓库源 nrm use anpm nrm ls 查看这里重点强调一下:npm-group中其实是包含npm-hosted和npm-proxy,npm-proxy的目的是保证我们自己的npm包中使用其他的npm包可以通过代码进行下载使用。而npm-hosted主要目的就是为了下载我们自己发布的npm包。4、在本地登录跟在npmjs.com发布是一样的,同样要先npm login,然后再npm publish这里可以发现,我的登录仓库源还没有修改过来,我当时设置的是npm-hosted,所以导致最后测试包的时候,在客户端进行安装下载包的时候就会报错这里的仓库源一定要设置成[npm-group]。5、发布网址查看6、最后本地测试// package.json "bin": { "create-ak-vue": "./bin/vue.js", "create-ak-vite": "./bin/vite.js" },两个文件中的代码比较简单然后安装成全局npm i aehyok-cli -g7、总结neuxs3可以说是一键就将npm私服生成了,然后自己根据需求进行少量的配置就可以使用了,非常棒。管理上也有权限安装进行控制。以及到最后体验直接使用,整个过程可以说是非常流畅,虽然自己也遇到了很多问题,但大部分都是自己不熟悉导致的。
前言 通过本文可以学习到,好几种分支之间合并代码的方式,让你可能对日后的代码合并不再惧怕。1、功能分支或者bug分支合并到主分支有时候我们开发一个功能可能会基于主分支创建新的分支,然后等功能开发完毕后,要合并到主分支。比如:功能分支位feat/village, 主分支是dev。我们要想将功能分支代码合并到主分支,那么我们首先就要切换到主分支。// 先切换的dev主分支 git checkout dev // 将feat/village分支的变更拉取到dev分支上来 git merge feat/village // 合并完成,即可推送到远程服务器 git push -u origin dev当然合并代码有时候难免就会有冲突,这个时候我们就需要先解决冲突然,解决完冲突后再进行推送。一般的冲突可能是这样的:你先拉取代码到你的本地分支,然后其他同事也有可能在改别的功能或者bug,但是他比你早合并代码到远程分支,这样在你完成功能或者修改完代码后,合并到远程分支的时候,很多时候有一些合并会进行自动合并,但也有很多时候有冲突需要手动解决。同时别人有可能也改过跟你相同的文件,相同的函数,这样就不可避免的就要手动解决冲突。\简单的冲突可能就是: 选择你本地的代码、或者选择远程分支的代码、再或者要同时选择本地代码并且要加上远程分支的代码。有时候可能合并之后代码存在问题,有可能要与共同修改的同事进行商量,重写冲突的那几行代码,因为我们可能同时修改实现了两个功能,要考虑如何兼容两个功能的问题。2、将其他分支的某个提交合并到当前分支// (正常情况直接合并成功,可能存在重复图需要手动解决) git cherry-pick commitid // 正常情况直接push即可,手动解决冲突需要commit 再进行push git push // 扩展阅读:还可以一起提交commitid-A到commitid-B 之间所有的内容修改 // http://www.ruanyifeng.com/blog/2020/04/git-cherry-pick.htmlcommitid 可以通过git log 来确定3、将其他分支的多个提交一起合并到当前分支这里其实有两种情况,一种是几个提交是分离的,不连续,没什么关系;另外一种则是几个题提交是连续的。不连续的合并 简单的方式就是通过上面的方式一个一个的合并,但是有一点麻烦,其实只要稍微变通就可以提交多个git cherry-pick commitidA commitidB commitidC git push连续的合并// commitidA的提交时间必须要早于commitidB // 这种提交不会包含commitid-A本身 git cherry-pick commitidA..commitidB git push //如果想包含commtidA也是可以的 git cherry-pick commitidA..commitid B git push4、两个git仓库进行代码和日志记录的合并// git@github.com:aehyok/blog1.git // git@github.com:aehyok/blog2.git // 以blog1仓库的main分支为基础,将blog2仓库的main分之合并到blog1仓库的main. //1.在blog1仓库下添加远程仓库blog2 git remote add blog2 git@github.com:aehyok/blog2.git //2.在blog1仓库下拉取数据到本仓库 git fetch blog2 //3.根据拉取将远程仓库的blog2的main分支,在本地创建blog2main git checkout -b blog2main blog2/main //4.切换回blog1的main分支 git checkout main //5.将本地分支blog2main合并到main分支 // 这两个仓库本来就是没有关联的,所以要加上参数 git merge blog2main --allow-unrelated-histories //最后如果没有冲突就结束了 // 如果有冲突就解决一下 git push5、总结四种我日常工作中使用过的合并场景,前面三种用过的比较多一点,第四种就用过一次,这些场景你有遇到过嘛?或者你还有其他场景的合并吗?
前言:上一篇文章学习的&和&&其实就是并行和串行的指令执行,文章链接为juejin.cn/post/712703… 本文主要来介绍一下几种执行串行命令和并行命令的方式1、第一种方式就是上一篇文章中介绍的&和&&,如果有不太明白的,可以点击前言中的链接去看一下,相信你可以学到一些知识,如果你已经非常熟悉,那就可以忽略了。2、第二种方式 npm-run-all首先这是一个npm包npm i npm-run-allnpm-run-all 、run-s 、run-p 后面两个其实是两个缩写形式,分别代码串行执行和并行执行, 而 npm-run-all 默认是串行执行,当然你可以添加参数npm-run-all 默认串行执行npm-run-all -- sequential:串行执行npm-run-all -- parallel:并行执行串行指令可以直接简化为以下:// chuanxing 当然名字不能相同,我这里好做比较所以就暂时写成相同"chuanxing": "npm run demo1 && npm run demo2 && npm run demo3", // 或 默认指令 其实也可以加默认参数 --sequential "chuanxing": "npm-run-all demo1 demo2 demo3", // 或简化指令 run-s "chuanxing": "run-s demo1 demo2 demo3",原来的并行指令可以简化为以下:// bingxing 当然名字不能相同,我这里好做比较所以就暂时写成相同"bingxing": "npm run demo1 && npm run demo2 && npm run demo3", //或 简化指令 run-p "bingxing": "run-p demo1 demo2 demo3", // 默认指令加参 --parallel "bingxing": "npm-run-all --parallel demo1 demo2 demo3"3、第三种方式 concurrently并行执行命令库这也是一个npm库,要先安装npm i concurrently并行执行命令的示例"start": "concurrently \"npm run demo1\" \"npm run demo2\" \"npm run demo3\""4、pre和postnpm run为每条命令提供了pre和post两个钩子(hook)。找个例子可能更好理解一些"predemo1": "node demo2.js", "demo1": "node demo1.js", "postdemo1": "node demo3.js",可以发现先执行的是predemo1钩子命令,这个其实就是前置钩子。然后开始执行demo1命令,这也是我们的主体命令了。最后会执行postdemo1钩子命令,这个其实就是后置钩子。5、总结学习几种方式如何去串行执行和并行执行命令某个命令的前置钩子命令和后置钩子命令理解前后置钩子和串行、并行便可以根据实际情况去变通组合,更方便、有利于写出符合实际情况的脚本
本文主要是基于mac下操作的,window下可能会有所偏差1、官网下载地址https://nodejs.org/en/download这里可以看到window、mac以及linux版本的安装文件以及源码文件。通常我们只需要下载window或者mac的安装包到本地再进行安装即可。2、安装及确认安装完毕后可以通过如下命令进行查看安装的版本。node -v // 查看nodejs安装是否成功,成功后会出现版本号 npm -v // 同样会出现npm版本这里其实还有一个查看对应版本的链接 nodejs.org/zh-cn/downl…3、window或mac下安装多版本nodejs因为有时候要维护老项目,所以必然的nodejs的版本就比较低,一开始我还是先卸载nodejs,然后再安装低版本的nodejs,被这样搞几次,后面就会觉得很烦哟,于是发现了在一个电脑上可以安装多个nodejs版本,并且有一个非常方便的管理nodejs版本小工具 nvm。4、nvm下载官网安装方式 github.com/nvm-sh/nvm/…执行如下命令curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash // 或 wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash这里下载如果Failed to connect to raw.githubusercontent.com port 443。通过网站查找IP:www.ipaddress.com 输入raw.githubusercontent.com找到IP地址 然后命令行中sudo vim /etc/hosts,在尾部添加185.199.108.133 raw.githubusercontent.comIP每隔一段时间可能会有变化,所以要根据自己当时的IP进行配置5、nvm配置下载完之后,查找是否有如下文件夹~/.bash_profile, ~/.zshrc, ~/.profile, or ~/.bashrc// 直接访问看文件是否存在 vim ~/.zshrc //不存在则创建 touch ~/.zshrc查看如下配置是否存在与.zshrc文件中,存在则不需要了// 如果不存在将如下配置添加到末尾 export NVM_DIR="$([ -z "${XDG_CONFIG_HOME-}" ] && printf %s "${HOME}/.nvm" || printf %s "${XDG_CONFIG_HOME}/nvm")" [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" # This loads nvm最后重新加载一遍配置文件source ~/.zshrc6、使用nvm// 检查是否可以打印出版本 nvm -v //查看本地已安装版本 nvm ls //查看所有远程服务器的版本 nvm ls-remote // 查看当前版本 nvm current // 安装node最新稳定版本 nvm install stable nvm install <版本号> //切换正在使用的版本 nvm use <version> //其他命令可查询 nvm help7、总结熟悉nodejs安装多个版本的nodejs如何管理切换访问地址异常时候的一种解决方案:将IP地址放到hosts中
前言话不多说,先来看看十分钟肝出来的博客吧。两个地址: 第一个自己部署的地址:vue.tuokecat.com/blog 第二个github pages地址:aehyok.github.io/blog最后本博客源码仓库地址:github.com/aehyok/blog1、搭建个人博客背景2020年9月底入行前端,2021年年初的时候就想着,之前三个月遇到的好多问题,或者看过的好多文章都已经连个毛都没有了。遇到相同或者类似的问题又需要进行百度或者google了。其实是一件挺痛苦的事情,浪费了自己的时间,关键是浪费了好多次,得不偿失。于是我就在github上新建了一个仓库,然后开始书写markdown记录笔记了。以前都是在印象笔记里直接富文本了,相对来说md文档见过很多,自己确实也没尝试过。然后从2021年1月1日开始就开始记录每天看到的文章或者遇到的问题了,这是我的**github记录仓库**,有兴趣的可以点进去看看,其实就是流水账哈哈,但是对自己来说还是挺有用的。把自己日常中用到的知识,哪怕一个链接或者一个关键字,把它记录下啦,对于回头看看,回顾知识还是很有益处的。其实一直有这样一个想法,想把自己的流水账记录,稍微整理一下,放在一个自己打造的个人博客下面,让知识不在孤零零的呆在哪里,有时候可能更方便自己,甚至可以启发很多同行......一直拖拖拖没有去实践,最近因为那条腿只能呆在家里,所以空闲时间就比较多。这事既然想起来了,就把它肝出来了,其实也没花费多长的时间。其实主要就是把过去自己的记录进行汇总整理今天再去趟医院,争取明天8月2日去上班了,已经三周没见过同事了,不知道公司是不是又裁员了,又或者这次该轮到我了 哈哈当然接下来从8月份开始,一有时间我就会重新梳理一下过去一年半的笔记记录,让很多知识呈现出来,对知识进行加工,成为我自己的财富,也许不久后的面试就要用到呢?刚开始还有一些犹豫一直想使用vuepress,因为之前也用vuepress搭建过,但后面不了了之了。现在是又想尝试新鲜的vitepress,在我正在犹豫的时候我看到一个非常亮眼的优点:可以在md文档里面混合使用vue3的组件,听说还可以全局引入,真的有点骚到爆了。有空的时候再去看看尤大大是如何将代码嵌入到markdown中执行的。2、尝鲜vitepress搭建// 1、创建项目并初始化package.json mkdir vitepress-starter && cd vitepress-starter pnpm init //2、安装vitePress和vue3.0 pnpm i vitepress vue -D //3、创建文件夹docs,并在其中添加一个md文档index.md mkdir docs && echo '# Hello VitePress' > docs/index.md //4、在package.json中添加scripts脚本 "scripts": { "start": "vitepress dev docs", "build": "vitepress build docs", } //5、通过命令运行项目 pnpm start //ok一个最简单的博客出现了。3、vitepress进阶3.1、基础使用很多知识官网已经讲的非常清楚了,详见官网地址:vitepress.vuejs.org/,跟着官网去操作稍加调试便可以实现。3.2、md中嵌入vue组件特性上面说过可以在md中直接添加vue的组件,来看看示例吧。<script setup> import middle from '../src/components/middle.vue' </script> <middle />引入的middle组件我在下面会详细说明。就这一项,可能对开发的来说就非常的友好了,因为有时候确实想做一些定制化的需求,尤其是对前端来说。3.3、md中自定义样式 之前我不清楚有没有,看vitePress官网可以在markdown下写样式// 在markdown文件中直接写就可以了 <style lang="sass" scoped> h2 color: red </style>我自己试一下确实不错,不过有时候想定制化样式不知道是否可以添加class,自己简单尝试了一下不行,不过现在都可以嵌入vue组件了,那么在组件中定制样式就非常的简单了<template> <div class="middle"> <h2 className="message"><span @click="jumpClick()">我参与源码共读活动两个月的时间,写了七篇文章,但更重要的是学到更多的知识。源码共读活动,有兴趣的来一起参与一下吧</span></h2> </div> </template> <script lang="ts" setup> const jumpClick = () => window.open('https://juejin.cn/post/7079706017579139102','_blank') </script> <style lang="scss" scoped> .middle { display: flex; align-items:center; justify-content: center; margin-top: 80px; color: var(--vp-c-brand); cursor: pointer; text-decoration: underline; &:hover { color: var(--vp-c-brand-dark); } } </style>其实这段代码就是我开头引用的middle组件。3.4、部署方面尝试使用github action 在推送代码时触发指定事件完成编译部署 aehyok.github.io/blog使用goole/zx编写脚本,在本地编译时执行脚本一键自动打包到服务器vue.tuokecat.com/blog4、畅想博客未来比如说我自己吧,接下来的八月份一有时间我就先把我之前整理的笔记记录,重新看一遍、整理汇总一下,更方便自己查看阅读。然后有时间会思考,接下来怎么样来给自己的博客添砖加瓦呢?比如:想加个登录,对接一部分接口,不知道能否调通,要去尝试再比如:统计一下每个markdown的浏览量,应该有插件可以支持再比如:是否可以添加一部分接口,通过权限控制一下显示再比如:某些没写好的文章,或者比较重要的笔记不想展示出来,是否可以对接控制再比如:给每个markdown记录下面添加一个评论系统,这样有一个互动,可能对来阅读的人来说更加友好,可以很好的跟博客作者互动再比如:在markdown里添加一个代码调试器,这样写博文时嵌入的代码会更加生动,当然有可能这个功能已经有了,只是我没看到而已再比如:源码共读活动,如何在博文中展示会对读者更加友好呢可以加入代码,本身就增加了很多的乐趣和想象的空间5、总结可以使用vitepress搭建一个属于自己的个人博客。如果你想简单直接使用模版,我的源码在此,主要去填充笔记就可以了。如果你有兴趣一起搞一点小玩意,小代码什么的,我们可以一起碰撞一下,欢迎来撩。最后希望不努力的我和你能多参加一下若川大佬的源码共读活动,原来我们也可以读源码了,原来不努力的我们变成努力的我们其实就只差一个转身,一个前端经验不足两年的弱鸡告诉你,真的真的可以学到很多东西。接下来有时候我会继续整理日常记录当然也会不断的尝试着为我的个人博客添砖加瓦,学习就是一个慢慢积累的过程,不要操之过急,慢慢的坚持下去,收获就自然而来了
1、前言查了掘金很多很多的文章,发现绝大部分文章告诉我的都是四个进程,都是比较久的文章了,没有我想要的答案,没办法硬着头皮深入学习一下,这一深入我就发现了浏览器更多的秘密。直接看图,先来看一下开启的进程到底有哪些,这里我是win10下运行的Chrome浏览器,并开启了一个标签页(其实就是咱掘金的首页)先说一个次要的插件进程(扩展程序),我win10电脑的Chrome浏览器中装了很多插件,所以这里才会有这么多的插件进程。这里本可以一个也没有。如果不包含插件进程,如图所示我的电脑当时就真的是开启了六个进程。这六个进程的大致总结如下:1、浏览器主进程2、GPU进程3、渲染进程4、网络进程(Network Service)5、存储进程(Storage Serverice)6、备用渲染程序进程如果你是来看结果的,那么结论已经有了,可以点个赞收藏做个笔记走人了。那接下来我们就来聊聊:1.进程是什么呢?2.为什么会有这么多的进程呢?3.这些进程又是怎么工作的呢?4.又是如何演变的呢?带着这几个疑问有兴趣的话,继续往下看看吧,也许你会有一些收获。2、进程和线程2.1、进程与线程前言中已经提到的进程,对我来说有一点陌生。其实我对进程和线程的理解一直模模糊糊的,所以索性去认真学习了一下,发现大佬有文章在此。如果有对进程和线程的理解不是很到位,或者想学习一下,可以看一下阮一峰大佬的精彩文章,终点就是看完你一定会懂,文章链接:[进程与线程的一个简单解释],(www.ruanyifeng.com/blog/2013/0…) ,这里我就不班门弄斧了。2.2、 进程与线程的区别和联系进程可以理解是应用程序的执行程序,线程则是存在于进程内部,并执行该进程程序的一部分功能。一个程序的运行可能至少包含一个进程,同时一个进程中至少包含一个线程,而线程是由进程开启和管理的。进程可以通过操作系统,启动另一个进程来执行不同的任务。此时,系统将为新进程分配不同的内存。如果两个进程间需要通信,他们可以利用 IPC(Inter Process Communication)的方式进行通信。许多应用程序都是以这种方式执行的,因此如果某个工作进程(例如一个选项卡)无响应,重启它,并不会影响相同应用程序的其他进程。1.进程是cpu资源分配的最小单位(是能拥有资源和独立运行的最小单位)2.线程是cpu调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)3.进程中如果任一一个线程出错,都有可能导致整个进程的崩溃。4.一个进程中的线程与线程之间是可以共享进程中的数据的。5.进程与进程之间的内容是相互隔离的,如果一个进程崩溃或者挂起了,是不会影响其他进程的(某些特殊的场景除外)。6.进程与进程之间的通讯是通过IPC(Inter Process Communication)机制。7.当进程关闭后,操作系统会回收进程中所占用的内存。8.想到最后一点,进程中的多个线程可以一起工作,也就是所谓的并行运行。2.3、多线程与单线程的区别假设现在有一个场景:现在页面有十个视频要上传1.方案一:十个视频一个接着一个进行上传,直到十个视频上传完毕。也就是串行上传(通过一个线程一直上传),耗费时间较长(十个视频大小一致,每个上传1分钟,十个上传完毕需要十分钟)2.方案二:十个视频,相同时间可以有三个视频一起上传,然后其中一个上传完毕,便可以继续上传其他视频,直到所有十个视频全部上传完毕为止。这就是并行上传(开启三个线程分别开始同时上传)在配置和宽带允许的情况下,上传的时间会大大缩短(视频大小与方案一一致,每次1分钟后同时上传完三个视频,下一次同样,三次之后上传完九个视频,剩余的一个将会由三个线程中的随意一个进行上传,总计4分钟)。总结:多线程的并行上传比单线程的串行上传优势非常明显。JS 中其实是没有线程概念的,所谓的单线程也只是相对于多线程而言。JS 的设计初衷就没有考虑这些,针对 JS 这种不具备并行任务处理的特性,我们称之为 “单线程”。 —— 来自知乎 “如何证明 JavaScript 是单线程的?” @云澹的回答3、浏览器多进程架构3.1、浏览器运行进程架构图回头看前言的截图,你便轻松可知浏览器的运行是多进程的,因为我就开启了一个浏览器,竟然有那么多的进程开始运行了。从上图可见有很多的进程,接下来我来说说每个进程都是干什么的:浏览器主进程:主要负责界面显示、用户交互、子进程管理。(无论开多少个标签页,只会有一个主进程)渲染进程:核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中,默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。(每个标签页一个渲染进程,特殊情况暂不考虑)对于前端来说最重要的一个进程:渲染进程。网络进程:主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程。(无论开多少个标签页,只会有一个网络进程)GPU 进程:GPU 的使用初衷是为了实现 3D CSS 的效果,只是随后网页、Chrome 的 UI 界面都选择采用 GPU 来绘制,这使得 GPU 成为浏览器普遍的需求。最后,Chrome 在其多进程架构上也引入了 GPU 进程。(无论开多少个标签页,只会有一个GPU进程)插件进程:主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响。(根据插件的数量,每个插件都拥有自己的插件进程)备用渲染进程:用于配合渲染进程的使用。基础服务进程(实用程序):存储进程:用于存储数据的,从原来浏览器主进程中分离出来的。Audio进程:用于音频文件的处理。网络进程:其实也属于基础服务进程,只是它实在太重要了,所以我上面单独拎出来了。\之前很多基础服务进程是包含在其他进程中的,现在正在做面向服务的未来架构,未来会将更多其他的模块服务,单独封装成独立的基础服务进程,更加方便于系统的调度和管理。3.2、浏览器进程架构演变简单总结一下浏览器进程架构的演变:1.单进程架构的浏览器时代不稳定--单进程中的插件模块或渲染模块以及其他模块的的崩溃就会引擎整个浏览器的崩溃。不流畅--单进程中的单线程运行,这就意味着同一时刻只能运行一个模块。不安全--在同一个进程的同一个线程中,通过插件或者脚本就可以轻松获得访问权限,使的恶意插件、恶意脚本非常的流行。2.早期的多进程架构的浏览器时代在浏览器进程的基础上,将插件模块和渲染模块单独封装为进程了,进程与进程之间通信就通过 IPC机制了,而渲染进程也是一个Tab页一个。解决不稳定--进程与进程之前是相互隔离、不可访问的,当一个页面或者插件崩溃时,并不会影响其他页面或插件。解决不流畅--JavaScript运行在渲染进程中,即使发生了阻塞,影响的也只是当前标签页。解决不安全--采用了多进程后,也可以很好的采用安全沙箱,相当于只能在沙箱中独自运行,并访问不到外部的资源和权限,这样恶意插件或恶意代码的影响就大大的降低。3.后期的多进程架构浏览器时代在早期多进程架构的基础上,又进行了升级。又将网络模块单独提取到网络进程中,同时又新增加了GPU进程满足新的需求。浏览器进程--负责界面显示、用户交互、子进程管理,同时提供存储等功能。渲染进程--负责HTML、CSS和JavaScript转换为用户可以与之交互的网页,排版引擎Blink和JavaScript引擎(V8)都是运行在该进程当中,默认情况下,Chrome会为每个标签页创建自己的渲染进程,渲染进程都是运行在烧香模式下。GPU进程--同时由于新需要的不断涌现,添加了GPU进程,GPU的初衷是为了实现CSS的3D效果,但是后来的网页UI界面都采用了GPU来进行绘制。网络进程--负责页面网络资源的加载,之前一直作为一个网络模块运行在浏览器主进程中,现在单独封装为一个独立的进程,并且一个浏览器只会存在一个网络进程,甭管它有多少个标签页。插件进程--每一个插件都拥有一个插件进程。4.现代面向服务架构的浏览器时代这个就是文章开头的最新版的多进程面向服务的架构,不过仍然在改造中,毕竟是一个庞大的过程,每隔一段时间有了新的进展,便会发布新的版本出来。注意:我这里讲的是算是最新的面向服务的浏览器多进程架构,浏览器从最开始的单进程,慢慢演变至今的过程,我这里就不总结了,已经有很多的文章介绍过了。3.3、详解最新进程架构过程根据上图所示,从进程的角度来分析一下,输入juejin.cn 之后发生的过程1.首先打开Chrome浏览器,浏览器会准备好浏览器主进程、GPU进程、网络进程、存储进程等,如有插件则会准备插件进程,其实打开浏览器之后,会默认打开一个空白的标签页,此时其实也准备好了渲染进程。2.在URL中输入juejin.cn,浏览器进程接收到用户输入的 URL 请求,浏览器进程便将该 URL 转发给网络进程。3.网络进程通过 IPC 机制接收到消息之后,在网络进程中发起真正的 URL 请求。4.接着网络进程接收到了响应头数据,便解析响应头数据,并通过 IPC 机制将数据转发给浏览器主进程。5.浏览器主进程接收到网络进程的响应头数据之后,通过IPC机制发送“提交导航 (CommitNavigation)”消息到渲染进程。6.渲染进程接收到“提交导航”的消息之后,便开始准备接收 HTML 数据,接收数据的方式是直接和网络进程建立数据管道。7.最后渲染进程会向浏览器主进程“确认提交”,这是告诉浏览器主进程:“已经准备好接受和解析页面数据了”。8.浏览器主进程接收到渲染进程“提交文档”的消息之后,便开始移除之前旧的文档,然后更新浏览器进程中的页面状态。9.渲染进程在渲染页面的时候,会与GPU进程打交道,用于UI界面的绘制。好了,对浏览器打开掘金首页的一个进程运行过程,做了一个简单的梳理,现在清晰多了4、总结理解了进程和线程的的概念和区别知道进程在浏览器中扮演的角色清晰了浏览器的架构演变过程以及详细了解了浏览器运行时的进程运行流程知道对于前端来说那个进程最重要,原来就是渲染进程,JavaScript的V8引擎就运行在此,所以很多之前学习的知识点,都是运行在这里的好了,不啰嗦了,有时间再去深入学习一下渲染进程中的那些事
前言通过本文可以学到哪些知识?如何在Chrome中安装vue-devtools插件创建vue3 demo项目,并调试一键打开对应组件文件通过关联图可清晰整个流程的大体脉络通过源码解剖原理vite项目如何进行调试使用我为什么看源码?以及看源码能学到什么呢?1、安装Chrome浏览器扩展插件vue-devtools1、打开Chrome浏览器,点击最右侧`三个点` 2、点击:更多工具 3、继续点击:扩展程序 4、然后点击:页面左上角`三横杠` 5、继续点击:打开Chrome 网上应用店 6、然后搜索框输入:`vue` 7、出现列表:选择'Vue.js devtools (https://vuejs.org)',点击此项进入 8、然后点击`添加至Chrome` 9、出现弹框后,点击`添加扩展程序`,执行完,便添加完毕注意了,这个插件也就是在开发的时候可以使用哟。2、创建项目并测试// 更新脚手架 npm update -g @vue/cli //查看版本 vue -V // 5.0.8,此版本下不需要配置任何便可调试打开对应组件文件 // 创建项目 vue create vue3-ts-demo // 选择一系列的模版组合,创建完毕,并安装依赖 // 直接通过VSCode打开此项目 // 直接运行 npm run serve浏览器打开页面会发现浏览器调试右侧出现一个标签页vue点击如图所示的图标,就是打开对应的组件文件。可能初次使用,点击好几次都没有响应,然后返回到VSCode编辑器后发现,如下图所示的错误,该怎么办呢?我现在使用的mac电脑,在VSCode中尝试快捷键command + shift + p,如果是window电脑可能是 Ctrl + Shift + p,然后输入shell,选择安装code。如下图:选中安装Code命令后,重新运行项目,然后再次点击如图箭头所示位置就会跳转到VSCode并打开对应的组件文件原理:其实就是通过code命令,进行打开对应的文件,你可以在项目根目录进行测试code package.json或者其他文件,稍等片刻便会打开该文件。3、一图胜千言其实调试完上面的代码以后,我才开始想这其中的原理到底是怎么实现的,心里还在嘀咕有那么神奇吗?其实作为菜如鸡的我一开始还真想不明白,于是我不断的阅读源码,一步一步的揭开了神秘的面纱,后来才发现也不过尔尔。这里需要搞清楚几个小问题,也是我在梳理的过程中花费时间比较长的疑惑。1.vue-devTools也算是Chrome插件,是Chrome DevTools扩展,开发者工具里面的Tab扩展页2.点击按钮发送的请求,然后通过webpack里的devServer进行请求拦截3.请求拦截后,调用尤大大写的小插件 launch-editor4.而launch-editor最终调用的child_process.spawn来执行命令5.最终执行vscode中的code命令(我上面写过如何去安装)4、从源码的角度解剖原理4.1、准备工作vue-devtools chrome浏览器插件已经安装通过vuecli 创建好了vue3的项目尤大大开源npm包 launch-editor尤大大开源Chrome DevTools插件4.2、vue-devTools 插件中点击按钮查看的代码当然就在devtools中按钮的模版代码<VueButton v-if="fileIsPath" v-tooltip="{ content: $t('ComponentInspector.openInEditor.tooltip', { file: data.file }), html: true }" icon-left="launch" class="flat icon-button" @click="openFile()" /> // tooltip显示的字符串 ComponentInspector: { openInEditor: { tooltip: 'Open <mono><<insert_drive_file>>{{file}}</mono> in editor', }, },通过代码可以发现按钮事件openFilefunction openFile () { if (!data.value) return openInEditor(data.value.file) }可以看到里面又调用了openInEditorexport function openInEditor (file) { // Console display const fileName = file.replace(/\\/g, '\\\\') const src = `fetch('${SharedData.openInEditorHost}__open-in-editor?file=${encodeURI(file)}').then(response => { if (response.ok) { console.log('File ${fileName} opened in editor') } else { const msg = 'Opening component ${fileName} failed' const target = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : {} if (target.__VUE_DEVTOOLS_TOAST__) { target.__VUE_DEVTOOLS_TOAST__(msg, 'error') } else { console.log('%c' + msg, 'color:red') } console.log('Check the setup of your project, see https://devtools.vuejs.org/guide/open-in-editor.html') } })` if (isChrome) { target.chrome.devtools.inspectedWindow.eval(src) } else { // eslint-disable-next-line no-eval eval(src) } }其中主要的代码便是src变量的赋值,以及通过eval或者target.chrome.devtools.inspectedWindow.eval来执行src字符串。src变量中的代码,不看错误处理的代码,其实也非常简单 就是一个fetch请求,其中变量SharedData.openInEditorHost就是一个/fetch(`/__open-in-editor?file=${encodeURI(file)}`).then(response => { if (response.ok) { console.log('File ${fileName} opened in editor') } else { const msg = 'Opening component ${fileName} failed' } })这里最重要的其实就是这个请求路径__open-in-editor4.3、再来看我们创建的demo项目vue3-ts-demo通过一个截图来看看代码位置代码其实是在node_modules/@vue/cli-service../serve.js,也就是运行一下命令后执行的脚本代码"serve": "vue-cli-service serve",执行完上述命令,会将__open-in-editor请求地址注册到devServer,后续如果有请求过来,地址刚好一样,便会调用下面的launchEditorMiddleware,并传递了一个匿名函数。const launchEditorMiddleware = require('launch-editor-middleware') // .....省略很多暂时用不到的代码 devServer.app.use('/__open-in-editor', launchEditorMiddleware(() => console.log( `To specify an editor, specify the EDITOR env variable or ` + `add "editor" field to your Vue project config.\n` )))4.4、中间件launch-editor-middleware上面launchEditorMiddleware最终使用的便是该npm包,里面只有一个文件,代码如下所示const url = require('url') const path = require('path') const launch = require('launch-editor') module.exports = (specifiedEditor, srcRoot, onErrorCallback) => { // specifiedEditor 传递进来的cnsole.log匿名函数 // srcRoot和onErrorCallback都为undefined if (typeof specifiedEditor === 'function') { onErrorCallback = specifiedEditor specifiedEditor = undefined } if (typeof srcRoot === 'function') { onErrorCallback = srcRoot srcRoot = undefined } // 获得当前执行node命令时候的文件夹目录名 // /Users/user/Desktop/aehyok/github/demo/vue3-ts-demo srcRoot = srcRoot || process.cwd() // 初始化先,并不执行 return function launchEditorMiddleware (req, res, next) { const { file } = url.parse(req.url, true).query || {} if (!file) { res.statusCode = 500 res.end(`launch-editor-middleware: required query param "file" is missing.`) } else { launch(path.resolve(srcRoot, file), specifiedEditor, onErrorCallback) res.end() } } }这里初始化的时候,并不会执行return function launchEditorMiddleware。等到下次有请求为__open-in-editor的时候,才会真正的调用 launchEditorMiddleware。4.5、点击open xxx path in editor看顶部引用const launch = require('launch-editor')4.6、最后的执行函数 launch-editor然后开始执行如下函数function launchEditor (file, specifiedEditor, onErrorCallback) { // 最重要的是解析出文件名 const parsed = parseFile(file) let { fileName } = parsed const { lineNumber, columnNumber } = parsed // 判断次文件是否存在 if (!fs.existsSync(fileName)) { return } // 根据guessEditor推断出当前使用的代码编辑器 const [editor, ...args] = guessEditor(specifiedEditor) // 如果editor,则会报错 if (!editor) { onErrorCallback(fileName, null) // 其中打印错误就是之前上面的截图 // colors.red('Could not open ' + path.basename(fileName) + ' in the editor.') return } //... 中间忽略很多的代码 // window系统下调用cmd命令行工具执行code if (process.platform === 'win32') { // On Windows, launch the editor in a shell because spawn can only // launch .exe files. _childProcess = childProcess.spawn( 'cmd.exe', ['/C', editor].concat(args), { stdio: 'inherit' } ) } else { // 其他平台则直接使用nodejs中childProcess模块的spawn,来执行code命令行 _childProcess = childProcess.spawn(editor, args, { stdio: 'inherit' }) } }至此整个调试的大致过程已经清晰明了了,不知道你看懂了没,如果没看懂,一定是我没表述清楚,不过整个思路我已经了然于胸了。5、通过vite 创建测试项目// 创建项目 pnpm create vite //项目名称 vite3-vue3-demo // 选择框架 vue // 选择一个变体 vue-ts //OK,项目创建完毕,安装依赖 pnpm i // 运行项目 pnpm dev这里我试过好几个办法都没有解决去给process.env.EDITOR赋值,可能是我打开方式不对,如果有看我文章有解决方案的,可以告知菜鸡一下,非常感谢。我这里就是直接修改node_modules中launch-editor中的guess.js// Last resort, use old skool env vars if (process.env.VISUAL) { return [process.env.VISUAL] } else if (process.env.EDITOR) { return [process.env.EDITOR] } return [null]将最后的return [null]改为 return ['code'],其实就是上面的process.env.EDITOR不知道如何赋值,我尝试了两种方式去修改,都没成功,这其中原因我还没找出来???6、总结通过调试源码发现,只要仔细一点稍微花点时间,原来也能看懂尤大写的代码,没有想象中的那么难,而且感觉逻辑非常清晰,阅读起来很优雅。所以大家如果有想看源码,或者参加若川源码共读活动的,一定要大胆一些,不要怂,事情真的没有那么难。有点目的性的阅读源码似乎更高效,这样针对性很强,不会大一统所有的源码都会过一下,时间一下子就过去了,每次带着一个小问题去看源码或许也是若川大佬的精髓所指。通过阅读源码,就是把看不懂的函数方法关键字等,不断的查漏补缺。或者在这里的用法或者写法不一样,等等各种超乎你想象的用法、场景...,收获真的是非常大,尤其是看完后再写一篇小文总结出来,真的就比读一遍别人写的收获要多好几倍的感觉。所以如果你还在犹豫自己看不懂,自己行不行等等借口,作为一个前端还不到两年经验的人告诉你,加加油相信自己,你完全可以的。最后一定要行动起来。写的文章不要总想着有好多好多的浏览量,又或者有几十几百上千的点赞,我的要求不高,首先是自己真的学到了,思路真的整理总结出来了,那么自己在遇到同样问题的时候回看也是非常的方便的。最后送给有需要的人士,哪怕只有一位同仁看了我的文章,也就证明了它的价值所在。谢谢在看的朋友,欢迎你一起来参加源码共读活动,学习真是一件快乐的事情,尤其是真的学到了,而不是浑浑噩噩的在混日子,当然自己之前浪费过太多的时间,假以时日,我定会.......
前言我之前好多次都是一步一步的安装eslint和prettier及相关依赖,一个配置文件一个配置文件的粘贴复制,并修改其中的相关配置。而且可能会在每个项目中都要去处理,如果项目工程规划化以后,eslint和prettier确实是项目少不了的配置。不知道你有没有像我一样操作过呢?那么有没有一种更简单的方式去处理呢?答案是我终于遇到了。通过若川大佬的源码共读活动发现了,真的是太棒了。本文以vite脚手架创建的项目为基础进行研究的,如果是其他脚手架创建的项目,那么就要自己去修改处理,但是原理是一样的。那么接下来,我就要来一探究竟,先看看如何使用,然后查阅一下它的源码,看看它到底是如何实现的呢?1、vite创建项目创建项目yarn create vite一顿操作以后项目就创建完毕了2、安装依赖yarn3、运行项目yarn dev4、运行初始化eslint和prettier命令yarn create vite-pretty-lint先来看没有执行命令前的文件目录再来看执行完命令后的文件目录可以发现文件目录中增加了eslint和prettier的相关配置,package.json中增加了相关的依赖、以及vite.config.xx文件也增加了相关配置,具体的文件变更可以查看github.com/lxchuan12/v…一个命令干了这么多事情,真的太优秀了。接下来我们就去看看这如此优秀的源代码吧2、整个过程的示意图通过大致的查看源代码,简单总结出来的代码执行过程示意图,仅供参考3、源码调试过程3.1、找到调试代码的位置通过package.json中的bin节点可以发现,yarn create vite-pretty-lint最终执行的便是lib/main.js中的代码"bin": { "create-vite-pretty-lint": "lib/main.js" },3.2、 开始调试的命令因为我们现在只是要执行lib/main.js这个入口文件,通过package.json的scripts 也没有发现执行命令,所以现在我们可以直接通过node来运行代码node lib/main.js调试成功的结果如下图所示3.3、 查看头部引入的模块chalk终端多色彩输出npm i chalk import chalk from 'chalk' const log = console.log // 字体背景颜色设置 log(chalk.bgGreen('chalk打印设置') ) // 字体颜色设置 log(chalk.blue('Hello') + ' World' + chalk.red('!')) // 自定义颜色 const custom = chalk.hex('#F03A17') const bgCustom = chalk.bgHex('#FFFFFF') log(custom('customer')) log(bgCustom('bgCustom'))执行效果如下图所示gradient 文字颜色渐变// 安装 npm i gradient-string // 引入 import gradient from 'gradient-string' // 使用 console.log(gradient('cyan', 'pink')('你好啊赛利亚欢迎来到编码世界')); console.log(gradient('cyan', 'pink')('你好啊赛利亚欢迎来到编码世界')); console.log(gradient('cyan', 'pink')('你好啊赛利亚欢迎来到编码世界')); console.log(gradient('cyan', 'pink')('你好啊赛利亚欢迎来到编码世界')); console.log(gradient('cyan', 'pink')('你好啊赛利亚欢迎来到编码世界'));执行效果如下图所示child_process node.js中的子进程。在node.js中,只有一个线程执行所有的操作,如果某个操作需要大量消耗CPU资源的话,后续的操作就需要等待。后来node.js就提供了一个child_process模块,通过它可以开启多个子进程,在多个子进程之间可以共享内存空间,可以通过子进程的互相通信来实现信息的交换。import { exec } from 'child_process'; exec('ls',(error, stdout,stderr)=> { if(error) { console.log(error) return; } console.log('stdout: ' + stdout) console.log('执行其他操作') })执行效果如下图所示fs fs用来操作文件的模块import fs from 'fs' // 同步的读取方法,用来读取指定文件中的内容 fs.readFileSync() // 同步的写入方法,用来向指定文件中写内容 fs.writeFileSync() path路径分类import path from 'path'; // 拼接路径 console.log(path.join('src', 'task.js')); // src/task.jsnanospinner命令行中的加载动画// 安装 npm i nanospinner // 引入模块 import { createSpinner } from 'nanospinner'; const spinner = createSpinner('Run test').start() setTimeout(() => { spinner.success() }, 1000)执行效果如下图所示(Run test在加载的一个效果)enquirer (utils.js文件)交互式询问CLI 简单说就是交互式询问用户输入npm i enquirer import enquirer from 'enquirer' let tempArray = ['major(1.0.0)','minor(0.1.0)', 'patch(0.0.4)', "customer" ] const { release } = await enquirer.prompt({ type: 'select', name: 'release', message: 'Select release type', choices: tempArray }) if(release === 'customer') { console.log(release, 'customer') } else { const targetVersion = release.match(/\((.*)\)/)[1] console.log(targetVersion, 'targetVersion') }执行效果如下图所示:先出来一个下拉选择,选择完后根据if判断进行输出3.4、 调试具体代码3.4.1、 main.js中的入口async function run() { // 所有的逻辑代码 } run().catch((e) => { console.error(e); });通过run函数封装异步方法,这样最外面调用run函数时可以通过异步方法的catch捕获错误异常。看一个小例子const runTest = async () => { console.log('Running test') throw new Error('run test报错了') } runTest().catch(err => { console.log('Error: ' + err) })执行后打印顺序如下Running test Error: Error: run test报错了可以发现catch中截获了异常接下来开始进入run函数了3.4.2、 打印色彩字体// 这个看上面的引入模块解析即可 console.log( chalk.bold( gradient.morning('\n🚀 Welcome to Eslint & Prettier Setup for Vite!\n') ) );3.4.3、 交互式命令行export function getOptions() { const OPTIONS = []; fs.readdirSync(path.join(__dirname, 'templates')).forEach((template) => { const { name } = path.parse(path.join(__dirname, 'templates', template)); OPTIONS.push(name); }); return OPTIONS; } export function askForProjectType() { return enquirer.prompt([ { type: 'select', name: 'projectType', message: 'What type of project do you have?', choices: getOptions(), }, { type: 'select', name: 'packageManager', message: 'What package manager do you use?', choices: ['npm', 'yarn'], }, ]); } try { const answers = await askForProjectType(); projectType = answers.projectType; packageManager = answers.packageManager; } catch (error) { console.log(chalk.blue('\n👋 Goodbye!')); return; }getOptions 函数根据fs.readdirSync读取项目工程template文件夹下的所有文件,并通过path.parse转换对象,来获取文件名称name。askForProjectType函数通过enquirer.prompt返回两个交互式命令行,供用户进行选择projectType选择项目类型: 【react-ts】 【react】【vue-ts】 【vue】packageManager选择项目包管理方式:【npm】 【yarn】3.4.4、根据交互命令行返回结果进行匹配模板假如我们上面选择的是[vue-ts]const { packages, eslintOverrides } = await import( `./templates/${projectType}.js` );/template/vue-ts.js模板中的代码(其中代码较多但一看就明白我就不贴了),就是export导出了两个固定的模板变量数组,packages则相当于要引入的npm模块列表,eslintOverrides这算是.eslintrc.json初始化模板。3.4.5、拼接变量数组const packageList = [...commonPackages, ...packages]; const eslintConfigOverrides = [...eslintConfig.overrides, ...eslintOverrides]; const eslint = { ...eslintConfig, overrides: eslintConfigOverrides };commonPackages是shared.js中预定义的公共的npm 模块eslint则是通过公共npm模块中的eslintConfig和上面选择的template/xxxx.js中的进行拼接组成。3.4.6、 生成安装依赖包的命令const commandMap = { npm: `npm install --save-dev ${packageList.join(' ')}`, yarn: `yarn add --dev ${packageList.join(' ')}`, };将packageList数组通过join转换为字符串,通过命令将所有拼接npm模块一起安装3.4.7、 读取项目的vite配置文件const projectDirectory = process.cwd(); const viteJs = path.join(projectDirectory, 'vite.config.js'); const viteTs = path.join(projectDirectory, 'vite.config.ts'); const viteMap = { vue: viteJs, react: viteJs, 'vue-ts': viteTs, 'react-ts': viteTs, }; const viteFile = viteMap[projectType]; const viteConfig = viteEslint(fs.readFileSync(viteFile, 'utf8')); const installCommand = commandMap[packageManager]; if (!installCommand) { console.log(chalk.red('\n✖ Sorry, we only support npm and yarn!')); return; }根据选择的项目类型,来拼接vite.config的路径,并读取项目中的vite.config配置文件上面用到了一个函数viteEslint,这个具体的实现可以去看shared.js中,主要就是读取文件内容后,传入的参数code,就是vite.config.ts中的所有字符通过babel的parseSync转换为ast。ast对象如下图所示对ast数据进行了一系列的处理后,再通过babel的transformFromAstSync将ast转换为代码字符串。对于babel处理这一块我也不太了解,有时间我得去加一下餐,具体的可以参考 juejin.cn/post/684490…3.4.8 执行命令、执行完将eslint和prettier配置重写const spinner = createSpinner('Installing packages...').start(); exec(`${commandMap[packageManager]}`, { cwd: projectDirectory }, (error) => { if (error) { spinner.error({ text: chalk.bold.red('Failed to install packages!'), mark: '✖', }); console.error(error); return; } const eslintFile = path.join(projectDirectory, '.eslintrc.json'); const prettierFile = path.join(projectDirectory, '.prettierrc.json'); const eslintIgnoreFile = path.join(projectDirectory, '.eslintignore'); fs.writeFileSync(eslintFile, JSON.stringify(eslint, null, 2)); fs.writeFileSync(prettierFile, JSON.stringify(prettierConfig, null, 2)); fs.writeFileSync(eslintIgnoreFile, eslintIgnore.join('\n')); fs.writeFileSync(viteFile, viteConfig); spinner.success({ text: chalk.bold.green('All done! 🎉'), mark: '✔' }); console.log( chalk.bold.cyan('\n🔥 Reload your editor to activate the settings!') ); });首先通过createSpinner来创建一个命令行中的加载,然后通过child_process中的exec来执行[3.4.6]中生成的命令,去安装依赖并进行等待。如果命令执行成功,则通过fs.writeFileSync将生成的数据写入到三个文件当中.eslintrc.json、.prettierrc.json、.eslintignore、vite.config.xx。4、npm init、npx印象里面大家可能对它的记忆可能都停留在,npm init之后是快速的初始化package.json,并通过交互式的命令行让我们输入需要的字段值,当然如果想直接使用默认值,也可以使用npm init -y。create-app-react创建项目命令,官网链接可以直接查看 create-react-app.dev/docs/gettin…//官网的三种命令 npx create-react-app my-app npm init react-app my-app yarn create react-app my-app //我又发现npm create也是可以的 npm create react-app my-app上述这些命令最终效果都是可以执行创建项目的同样的vite创建项目的命令//官网的命令 npm create vite@latest yarn create vite pnpm create vite // 指定具体模板的 // npm 6.x npm create vite@latest my-vue-app --template vue //npm 7+, extra double-dash is needed: npm create vite@latest my-vue-app -- --template vue yarn create vite my-vue-app --template vue pnpm create vite my-vue-app --template vue可以发现vite官网没有使用npx命令,不过我在我自己电脑上尝试了另外几个命令确实也是可以的npx create-vite my-app npm init vite my-app通过上面的对比可以一个小问题,yarn create去官网查了是存在这个指令的,官网地址可看 classic.yarnpkg.com/en/docs/cli…而对于npm create这个命令在npm官网是看不到的,但是在一篇博客中发现了更新日志意思就是说npm create xxx和npm init xxx 以及yarn create xxx效果是一致的。那么我们来本文的命令行// 我们是通过npm安装的,并且包名里是包含create的 npm i create-vite-pretty-lint // 那么以下几种方式都可以使用的 npm init vite-pretty-lint npm create vite-pretty-lint yarn create vite-pretty-lint npx create-vite-pretty-lint再来看一下npx假如我们只在项目中安装了vite,那么node_modules中.bin文件夹下是会存在vite指令的如果我们想在该项目下执行该命令第一种方式便是第二种方式就是直接在package.json的scripts属性下关于npx的详细说明可以看一下阮一峰大佬的精彩分享 www.ruanyifeng.com/blog/2019/0…5、总结npm init xxx的妙用,以及对npx的了解,感觉对package.json的每一个属性,可以专门去学习一下对于自动添加eslint和prettier配置的原理分析.eslintrc.json、.eslintignore、.prettierrc.json算是直接新增文件,处理相对简单一些最重要的学习点:对vite.config文件在原有基础上的修改,这里就涉及到了AST抽象语法树6、加餐 V8下的AST抽象语法树有兴趣的话可以看看我前几天刚刚总结的关于V8引擎是如何运行JavaScript代码的,其中就涉及到关于AST的部分juejin.cn/post/710941…。接下来有时间我会简单的把AST详细的学习一下,查了很多资料发现AST还是非常重要的,无论是babel、webpack、vite、vue、react、typescript等都使用到了AST。
组件库在添加一个组件的时候,可能会涉及到很多初始化的问题,比如要添加组件库入口文件、组件的说明文档文件、demo例子的入口文件,单元测试的入口文件等等那么通过本文可以了解到一个命令来处理这些文件的初始化,省去了很多的手动处理1、克隆代码2、查看package.json文件3、运行命令4、调试命令5、调试npm run init aehyok 命令6、可自行调试npm run init aehyok del7、总结1、克隆代码git@github.com:Tencent/tdesign-vue.git2、查看根目录下的package.jsonscripts脚本命令很多dependencies依赖不少devDependencies 依赖巨多东西太多我们今天重点就来学习一下其中的init命令,实际执行的命令为node script/init3、运行命令(新增开发组件时初始化的文件)3.1.安装依赖npm i3.2.本地运行npm run init aehyok运行结束后可以发现项目中多了一些文件,截图如下这就是新增一个开发组件时,可以通过命令的方式,一键将各个文件进行初始化,这样其实节省了很多新建文件的重复工作,以及防止某些文件的遗漏,不得不说碉堡了。那接下来我们看看具体的逻辑是什么样的4、调试命令在vscode中通过使用快捷键:Ctrl + Shift + P,出现以下截图Auto Attach 也就是第一个Debug: 奇幻开关自动附加,然后鼠标点击或者通过Enter选中,选中后效果如截图选中智能即可。接着我们打开终端命令行在命令行中输入npm run init aehyok,同时我们找到script/init/index.js文件,在文件末尾的init()位置打上断点。看到如上所示截图,说明我们的调式命名成功了。5、调试npm run init aehyok命令5.1、init()算是index.js入口函数const [component, isDeleted] = process.argv.slice(2);process.argv读取命令行的参数 第一个索引为0的是Nodejs的路径 第二个索引为1的是当前js文件的所在路径 第三个也就是索引为2之后的才是我们命令行中自己定义的 通过定义两个变量 component和 isDeleted,获取process.argv的第三和第四个参数5.2、获取参数后进行判断if (!component) { console.error('[组件名]必填 - Please enter new component name'); process.exit(1); }如果变量component为空,则进行提示错误,并退出命令行的执行5.3、获取路径const cwdPath = process.cwd(); const indexPath = path.resolve(cwdPath, 'src/index.ts');通过process.cwd()获取当前项目的工作目录(相当于根目录) 然后通过path.resolve拼接src下index.ts的完整路径5.4、获取将要添加的文件模板对象const config = require('./config'); const toBeCreatedFiles = config.getToBeCreatedFiles(component);与index.js同目录下另外一个文件config.js,是一个目录为key,将要添加的文件列表为值的一个数组,其中 files属性则是要添加的文件。 通过传入一个组件名称,生成的对象如下图所示5.5、判断是添加还是删除if (isDeleted === 'del') { deleteComponent(toBeCreatedFiles, component); deleteComponentFromIndex(component, indexPath); } else { addComponent(toBeCreatedFiles, component); insertComponentToIndex(component, indexPath); }通过isDeleted来判断,这里我们的命令行参数中并未传值,所以下一步开始执行添加和插入的操作5.6、addComponent添加组件我们重点来看一下addComponent函数,先是通过Object.keys来循环上图中的keys值。function addComponent(toBeCreatedFiles, component) { // At first, we need to create directories for components. // 循环上图中五个路径的数组对象 Object.keys(toBeCreatedFiles).forEach((dir) => { // 拼接路径 const _d = path.resolve(cwdPath, dir); // 根据拼接路径,如果不存在,先创建路径文件夹 fs.mkdir(_d, { recursive: true }, (err) => { if (err) { utils.log(err, 'error'); return; } console.log(`${_d} directory has been created successfully!`); // Then, we create files for components. // 从路径模板对象中找到对应的key值 const contents = toBeCreatedFiles[dir]; // 获取当前节点的files数组进行循环 contents.files.forEach((item) => { // item为object对象时 if (typeof item === 'object') { if (item.template) { outputFileWithTemplate(item, component, contents.desc, _d); } } else { // 看到了有一个e2e test就是一个字符串 const _f = path.resolve(_d, item); createFile(_f, '', contents.desc); } }); }); }); }5.7、outputFileWithTemplate接下来再来看item为object时的函数,这里面涉及到一个简单的函数getFirstLetterUpper,将传入的字符串转换为首字母大写。再来看一下lodash中的template方法,看一个例子应该就明白了。更多的用法可以去 www.lodashjs.com/docs/lodash…// 通过分隔符创建编译模板然后通过变量进行替换,骚的一批 var compiled = _.template('hello <%= user %>!');\ compiled({ 'user': 'world' }); 最终打印出来的是hello world接下来继续看整个函数function outputFileWithTemplate(item, component, desc, _d) { const tplPath = path.resolve(__dirname, `./tpl/${item.template}`); let data = fs.readFileSync(tplPath).toString(); const compiled = _.template(data); data = compiled({ component, upperComponent: getFirstLetterUpper(component), }); const _f = path.resolve(_d, item.file); createFile(_f, data, desc); }第一行的代码又是来拼接路径的,看了一下tpl文件夹下的文件竟然是模板字符串,应该是用来生成初始化文件的第二行通过fs.readFileSync来读取模板字符串然后下面就是lodash的templte方法和compiled方法,就是根据预设变量进行模板字符串替换替换完毕后,就要通过方法createFile生成文件了function createFile(path, data = '', desc) { fs.writeFile(path, data, (err) => { if (err) { utils.log(err, 'error'); } else { utils.log(`> ${desc}\n${path} file has been created successfully!`, 'success'); } }); }就是通过fs.writeFile函数将替换后的模板字符串,一起生成并写入到文件中去,这样addComponent方法算是执行完毕了5.8 同理看一下deleteComponent其中包含一个 getSnapshotFilesfunction getSnapshotFiles(component) { return { [`test/unit/${component}/__snapshots__/`]: { desc: 'snapshot test', files: ['index.test.js.snap', 'demo.test.js.snap'], }, }; }通过函数可以发现主要是执行单元测试的时候生成测试的快照文件 snapshots,这里我们就简单的提一下(实际上是我去查了一下),不做过深的学习。接着看主函数function deleteComponent(toBeCreatedFiles, component) { const snapShotFiles = getSnapshotFiles(component); const files = Object.assign(toBeCreatedFiles, snapShotFiles); Object.keys(files).forEach((dir) => { const item = files[dir]; if (item.deleteFiles && item.deleteFiles.length) { item.deleteFiles.forEach((f) => { fs.existsSync(f) && fs.unlinkSync(f); }); } else { utils.deleteFolderRecursive(dir); } }); utils.log('All radio files have been removed.', 'success'); }将快照的文件夹及相关文件对象,一起合并到toBeCreatedFiles上,这个在上面说过可以回头查看。 接下来循环files,对数组中的每个对象也就是每个文件夹下的文件进行遍历的。这里发现deleteFiles属性是不存在的,唯一存在的地方是关于ssr的地方,这里也做了注释处理所以直接看function deleteFolderRecursive(path) { // 先判断路径是否存在 if (fs.existsSync(path)) { // 路径存在则读取path路径下的所有文件,进行循环 fs.readdirSync(path).forEach((file) => { const current = `${path}/${file}`; // 如果current是一个路径目录则进行递归调用 if (fs.statSync(current).isDirectory()) { deleteFolderRecursive(current); } else { // 如果是个文件则删除即可 fs.unlinkSync(current); } }); // 上面文件删除了,再进行目录的删除 fs.rmdirSync(path); } }5.9 字符串的替换函数replace// last import line pattern const importPattern = /import.*?;(?=\n\n)/; const cmpPattern = /(?<=const components = {\n)[.|\s|\S]*?(?=};\n)/g; let data = fs.readFileSync(indexPath).toString(); data = data.replace(importPattern, (a) => `${a}\n${importPath}`).replace(cmpPattern, (a) => `${a} ${upper},\n`); fs.writeFile(indexPath, data, (err) => { if (err) { utils.log(err, 'error'); } else { utils.log(`${desc}\n${component} has been inserted into /src/index.ts`, 'success'); } });上面是两个正则,通过字符串的replace函数来替换掉字符,替换后将字符串再写会源文件。5.10 deleteComponentFromIndex删除的逻辑跟添加的逻辑一致6、总结学习到一个命令创建组件库的初始化文件lodash的template、compiled 模板字符串的编译替换生成想要的字符串,再写入文件对于nodejs的删除文件、创建文件、删除文件夹、创建文件夹、写入文件等操作,本质对fs的操作的熟悉关于两个正则通过repace替换的第二个正则理解有点费解,有时间再学习一下正则表达式
本文主要通过代码来展示,如何封装选项式Api的生命周期钩子函数(骚操作)和组合式Api的生命周期钩子函数,以及他们之间的不同之处。1、一个组件中同时使用组合式api和选项式api相信玩过vue3的同学们,大部分应该都了解或者使用过vue2吧,那么对选项式的api就不会陌生,或者你不熟悉名字,看一下下面的代码就非常清楚了。<template> <div> Test </div> </template> <script> // 选项式api代码 export default { name: 'Test', mounted() { console.log('选项是Api: mounted 第一个') }, mounted() { console.log('选项是Api: mounted 第二个') }, } </script> <script setup> // 组合式api代码 import { onMounted } from 'vue' onMounted(() => { console.log('组合式Api:Setup中的onMounted 第一个') }) onMounted(() => { console.log('组合式Api:Setup中的onMounted 第二个') }) </script>执行的结果是// 组合式Api:Setup中的onMounted 第一个 // 组合式Api:Setup中的onMounted 第二个 // 选项是Api: mounted 第二个可以发现组合式Api的生命周期在Vue3中会顺序执行,而选项式Api的生命周期钩子只会执行最后一个,也就是存在覆盖的问题,但是在选项式Api中可以设置组件的name属性。2、看一下vue-router中的钩子函数<template> <div> <button type="button" class="btn btn-primary" @click="jumpClick">跳转到详情页</button> </div> </template> <script> export default { name: 'Test', beforeRouteEnter(to, from, next) { next(vm => { console.log('选项式Api:vue-router中的beforeRouteEnter钩子函数') }); }, // beforeRouteLeave(to, from, next) { // next(vm => { // console.log('选项式Api:vue-router中的beforeRouteEnter钩子函数') // }); // }, } </script> <script setup> import { onMounted } from 'vue' import { useRouter, onBeforeRouteLeave } from 'vue-router' const router = useRouter() onBeforeRouteLeave ((to, from) => { console.log('组合式Api:vue-router中的onBeforeRouteLeave') }) const jumpClick = () => { router.push('test-detail') } </script>刷新页面后,页面中只有一个按钮,并查看控制台console.log,你可以发现选项式api中的beforeRouteEnter钩子函数执行了。这个钩子函数比较特殊。在渲染组件前调用,也就是组件实例还没被创建。 点击按钮后跳转到另外一个测试页面,然后可以看到控制台执行了,组合式Api中的 onBeforeRouteLeave钩子函数的console.log。在选项式Api中我注释了一段代码: 写在这里不执行的,在下面setup生命周期执行是没问题的,现在还不清楚是不是我代码写的那里有什么问题????3、封装组合式Api中的钩子函数<script setup> import { onMounted } from 'vue' import { useRouter, onBeforeRouteLeave } from 'vue-router' import { useCompositionHooks } from '@/hooks/useHooks' onMounted(() => { console.log('在本组件执行,组合式Api: onMounted') }) const router = useRouter() onBeforeRouteLeave ((to, from) => { console.log('组合式Api:vue-router中的onBeforeRouteLeave') }) useCompositionHooks() const jumpClick = () => { router.push('test-detail') } </script>以及useHooks文件中的封装export function useCompositionHooks() { onMounted(() => { console.log('封装起来的组合式Api: onMounted') }) onBeforeRouteLeave ((to, from) => { console.log('封装起来的组合式Api:vue-router中的onBeforeRouteLeave') }) }就是添加了两个钩子函数的封装,我们来看看执行情况// 在本组件执行,组合式Api: onMounted // 封装起来的组合式Api: onMounted再来看一下点击跳转按钮的执行//组合式Api:vue-router中的onBeforeRouteLeave // 封装起来的组合式Api:vue-router中的onBeforeRouteLeave在组合式api的执行过程中,会根据代码的执行顺序进行,钩子函数可多次注入,但会根据代码前后顺序调用。4、封装选项式api的钩子函数(骚操作一下)<script> import { useOptionsHooks } from "@/hooks/useHooks" const useOptions = useOptionsHooks('test') export default { beforeRouteEnter(to, from, next) { next(vm => { console.log('在本组件执行,选项式Api: beforeRouteEnter') }); }, ...useOptions, } </script>再来看一下useHooks文件中的封装export function useOptionsHooks(componentName: string) { return { name: componentName, beforeRouteEnter(to: any, from: any, next: any) { next(() => { console.log('封装选项式Api:vue-router中的beforeRouteEnter钩子函数') }); }, } }执行结果如下,页面刷新后// 封装选项式Api:vue-router中的beforeRouteEnter钩子函数骚操作:通过对外封装一个useOptionsHooks, return返回的就是选项式api使用的钩子。然后再export default 中 通过对函数调用后的对象进行解构展开如果我们将export default中的代码位置进行调换export default { ...useOptions, beforeRouteEnter(to, from, next) { next(vm => { console.log('在本组件执行,选项式Api: beforeRouteEnter') }); }, }执行结果如下// 在本组件执行,选项式Api: beforeRouteEnter会根据当前代码的顺序位置,后面的代码如果包含前面的钩子函数,则会把前面声明的钩子函数内容覆盖掉。5、总结vue3组合式api,本身封装通用性就更强,现在对钩子函数的独立封装又有了新的认识vue3如果想设置组件的name名称,则可以在该组件位置,添加一个选项式api包裹设置name即可。如果要使用 vue-router中的 beforeRouteEnter钩子函数,就得在选项式api中进行声明和使用。vue3 选项
自从遇到了google/zx,很快我就将原先使用shell脚本的代码,全部转换到了google/zx相当于使用前端js来写脚本了,对前端来说实在太友好了使用zx编写脚本,可以随心所欲的使用nodejs模块类库,我的demo使用mysql类库去将日志写入到mysql数据库, 当然你可以调用fs-extra,写入到本地文件当中,选择性很多了使用mjs模块,可以像使用前端js代码一样轻松使用 import和 export引用zx中的$符号,可以随意使用shell脚本中的所有命令同时使用typescript来写zx的脚本,很是不错哟,不过在import时需要写成 ’./xxx.mjs‘,其实调用的就是mts可以在代码中随意调用 npm、 yarn、 pnpm等,以及git pull 、git push 等等。可以在代码中更好的编写脚本的逻辑控制,虽然在shell脚本中页可以实现,但对于前端工程师来说,用js这简直就是so easy的事情。完美的使用await async来控制 同步和异步代码的执行顺序1、之前通过shell脚本写的打包脚本脚本入口文件地址 :github.com/aehyok/2022…克隆下来,有兴趣的可以下载下来看看,cd到本目录下就可以进行测试,将十五个应用编译后的文件自动复制到测试环境sh build-all.sh -v 2.5.1.006 -p awpqc |tee build-log.txt -v 2.5.1.006 版本号设置 -p awpqc 分别代表五个项目2、我通过zx进行了重写如今我用google/zx(TypeScript)写的代码入口地址:github.com/aehyok/2022…先说一下背景:PC:主应用 + 八个子应用(全部是vue3)APP: 主应用(vue2) + 两个子应用(vue3)Other: P、Q、W 其他三个都是一个应用就完事的算一下,9+3+3=15个应用 我这脚本就是为了方便打包这15个应用3、怎么做的呢十五个应用每次都到对应的目录去yarn build,或者pnpm build,会相当麻烦,而且会有遗漏的可能。项目在我本地都是可以运行的,所以这里我省去了, npm install 或者 yarn 或者pnpm i 。通过我的小demo,便可以通过一个命令 pnpm build:all,将上面说的15个应用全部打包完毕,放到指定服务器上。然后可以查看log日志,确认报错信息,或者直接线上测试查看发布情况。如果有个别的子应用出现问题,可以单独去打包子应用即可。对每个子应用的打包目录,做了调整,共同打包到一个release目录,将这个目录作为一个代码仓库,方便回退和记录的。过程:(这里面暂时使用的命令工具为yarn)1、到指定的git仓库目录,拉取代码 cd path、git pull2、安装依赖 (此步骤省略) yarn3、代码仓库只有一个应用的直接编译应用 yarn build4、代码仓库包含多个应用的 需要先到指定的子应用目录 cd path, 再进行yarn build5、编译结束,根据版本号version 给代码仓库打上tag标签 git tag git push6、将编译后的文件拷贝到服务器指定的目录 scp -r7、所有的代码仓库都编译上传完毕以后,要将release代码仓库进行 git add . git commit git push,然后再打上对应的tag标签,推送到服务器4、实际操作(根据上面的过程进行一步一步的代码实现)1、拉取代码// 精简版代码(不包括写日志) export const gitPullBy = async(name: string, path: string) => { try { const gitPullInfo = await $`cd ${path}; git pull;`; if (gitPullInfo.exitCode === 0) { console.log('git pull 拉取代码成功') } } catch { } };2、安装依赖这一步按照道理,拉取代码后,要进行pnpm i,安装依赖的过程。暂时通过手动挡处理:保证了项目在本地开发环境能够运行,也就是说依赖肯定安装好了,要不然都跑不起来,自动化任务或者脚本里,难道能判断依赖是否安装过,或者说有新增依赖,可以通过什么方式来监测吗?3、直接编译子应用// 精简版代码 export const yarnBuildBy = async (path) => { try { const buildInfo = await $` cd ${path};yarn build;`; if (buildInfo.exitCode === 0) { console.log(`yarn build end success`); } } catch(error) { } }4、多个子应用批量编译通过promise.all并发一起执行多个字应用的编译// 精简版代码 export const yarnBuildChildList = async(list) => { try { const result =await Promise.all( list.map((item) => { return $`cd ${item}; yarn build`; }) ) if(result) { console.log('all', 'result') } } catch { } }5、编译结束,根据版本号打tag标签首先根据版本号判断是否已经存在相对应的version tag如果存在的话,先删除标签;如果不存在,直接打tag标签最后git push 推送到代码仓库即可// 全过程代码 export const gitTag = async () => { const { name } = global.project const path = baseUrl + name // 判断是否已经存在tag标签 const isExist = await isExistTag(path) console.log(isExist, 'isExist') if (isExist) { // 如果存在泽进行删除tag标签的操作 let isSuccess = await deleteTag(path); if (isSuccess) { await addTag(path, isExist); } else { oneLogger(`delete tag [${global.version}] error`); } } else { await addTag(path,isExist); } }; const isExistTag = async (path) => { const result = await $` cd ${path};git tag;`; console.log("判断是否存在tag", result); if (result && result.exitCode === 0) { let array = result.stdout.split("\n"); if (array.length > 0 && array.includes(global.version)) { return true; } return false; } }; const deleteTag = async (path) => { oneLogger(`delete tag [${global.version}] start`); const result = await $` cd ${path}; git tag -d ${global.version}; git push origin :refs/tags/${global.version}`; if (result.exitCode === 0) { oneLogger(`delete tag [${global.version}] end success`); return true; } return false; }; /** * * @param {*} path 路径 * @param {*} isExist 0为不存在,直接创建的;1为已存在删除的,重新创建 */ const addTag = async (path, isExist) => { const result = await $`cd ${path}; git tag -a ${global.version} -m 'chore:version ${global.version}版本号'; git push origin ${global.version};`; if (result && result.exitCode === 0) { if (isExist) { oneLogger(`re create tag [${global.version}] success`); } else { oneLogger(`create tag [${global.version}] success`); } } }; //调用写入mysql日志的 const oneLogger = (info) => { console.log(info); const { name } = global.project writerLog(name, info, global.version); };6、拷贝文件到指定服务器这里如果想更方便一些,可以把本地的ssh生成的公钥拷贝到相应的linux服务器的ssh目录export const copyFile = async(path: string) => { try { const result = await $`scp -r /e/work/git/dvs-2.x/release/cms/${path}/* root@139.9.184.171:/usr/local/sunlight/dvs/dvs-uis/${path}/` if(result.exitCode === 0) { oneLogger(`copy file [${global.version}] end success`) } else { console.log("fail", $`$?`); } } catch { oneLogger(`copy file [${global.version}] end error`) } } const oneLogger = (info) => { console.log(info); const { name } = global.project writerLog(name, info, global.version); };7、整理所有项目编译后的文件对于打包生成的文件进行release目录整理,当然你可以全部编译打包完,再一起copy到指定的服务器,根据需要都可以去调整。// 这一步有打包的代码需要进行git push export const gitPushBy = async(name: string, path: string) => { try { await writerLog(name, `git push start`, global.version); // const message=`build:前端${name} -- commit-version:${global.version}` const message=`build:前端app、qrocde、wechat、park、console(child)commit-version:${global.version}` const result = await $`cd ${path}; git add . ; sleep 2; git commit -m ${message}; git push origin;` if(result && result.exitCode === 0 ) { await writerLog(name, `git push end success`, global.version); } else { await writerLog(name, `git push error: ${result.stderr}`, global.version); } } catch (error){ console.log(error, 'error') if(error.stdout.includes('nothing to commit, working tree clean')) { await writerLog(name, `git push nothing to commit`, global.version); } await writerLog(name, `git push error`, global.version); } }5、在使用过程中的问题1、 pre...和post命令的使用"prebuild:all": "ts-node-esm pull.mts -v 4.5.3.007", "build:all": "concurrently \"pnpm build:app\" \"pnpm build:pc\" \"pnpm build:wechat\" \"pnpm build:park\" \"pnpm build:qrcode\" ", "postbuild:all": "ts-node-esm push.mts -v 4.5.3.007",在执行命令时执行 yarn build:all,那么会先执行命令 yarn prebuild:all, 执行完以后,才会真正执行yarn build:all, 执行完yarn:build:all以后,会执行yarn: postbuild:all。也就是pre为提前执行的钩子,post为结束后执行的钩子。但是如果使用pnpm执行 就不会执行pre和post钩子。(这里还没找到问题!!!!!)2、concurrently,并行运行脚本,使用前先安装依赖 pnpm i concurrently"build:all": "concurrently \"pnpm build:app\" \"pnpm build:pc\" \"pnpm build:wechat\" \"pnpm build:park\" \"pnpm build:qrcode\" ",3、在命令行中 & 为并行, && 串行,在pnpm安装的依赖中行不行,所以使用了concurrently"build": "ts-node-esm index.mts -v 4.5.3.007 -p app & ts-node-esm index.mts -v 4.5.3.007 -p pc & ts-node-esm index.mts -v 4.5.3.007 -p wechat & ts-node-esm index.mts -v 4.5.3.007 -p park & ts-node-esm index.mts -v 4.5.3.007 -p qrcode",这样使用还是不能并行,pnpm build,或者yarn build,都一样(这里也没找到问题!!!!!)6、执行日志管理我这里暂时是通过mysql库,写入的数据库日志1、实例化数据库链接对象let _conn = mysql.createConnection({ // 创建mysql实例,这是我的个人数据库可以访问,尽量只去查看 host: "139.159.245.209", port: "3306", user: "meta", password: "meta", database: "meta", });2、通过实例链接数据库_conn.connect(function (err) { if (err) { console.log("fail to connect db", err.stack); throw err; } // 这里正真连接上数据库了。 console.log( "链接成功--db state %s and threadID %s", _conn.state, _conn.threadId ); logDbStat(); //此为记录数据库链接的线程和状态 }); const logDbState = function () { console.log( "db state %s and threadID %s", _conn.state, _conn.threadId ); };3、用完后关闭链接const close = (conn) => { conn.end((err) => { if (err) { console.log("error ", err); } else { console.log("关闭成功", conn.state, conn.threadId); } }); };4、最后返回一个Promise 方便处理和调用return new Promise((resolve, reject) => { try { let sqlParamsList = [sql]; if (params) { sqlParamsList.push(params); } _conn.query(...sqlParamsList, (err, res) => { if (err) { reject(err); } else { resolve(res); close(_conn); } }); } catch (error) { reject(error); } });5、封装方法进行调用export const writerLog = async (project, content, version) => { let id = shortid.generate() await executeSql("INSERT INTO CiCdLog values(?,?,?,?,null,null)", [id, project, content, version, ]); };6、在使用的地方进行import导入,然后调用即可import { writerLog } from "./sql-helper.mjs" // 传递三个参数即可 await writerLog(name, `git push error`, global.version);7、测试命令行编译一个pc代码仓库,一个主应用和两个子应用,脚本所在代码仓库链接地址github.com/aehyok/2022…yarn build执行yarn build的时候,会先调用 prebuild命令然后执行完yarn build之后,会调用postbuild命令通过代码产生的写入日志,如下截图所示8、总结zx可以处理很多的事情,这里我就不一一列举了,有兴趣的可以继续深入学习了解探索一下,或许也会发现新的大陆。
1、通过本文可以了解到vue3 keepalive功能2、通过本文可以了解到vue3 keepalive使用场景3、通过本文可以学习到vue3 keepalive真实的使用过程4、通过本文可以学习vue3 keepalive源码调试5、通过本文可以学习到vue3 keepalive源码的精简分析1、keepalive功能keepalive是vue3中的一个全局组件keepalive 本身不会渲染出来,也不会出现在dom节点当中,但是它会被渲染为vnode,通过vnode可以跟踪到keepalive中的cache和keys,当然也是在开发环境才可以,build打包以后没有暴露到vnode中(这个还要再确认一下)keepalive 最重要的功能就是缓存组件keepalive 通过LRU缓存淘汰策略来更新组件缓存,可以更有效的利用内存,防止内存溢出,源代码中的最大缓存数max为10,也就是10个组件之后,就开始淘汰最先被缓存的组件了2、keepalive使用场景这里先假设一个场景: A页面是首页=====> B页面列表页面(需要缓存的页面)=======> C 详情页 由C详情页到到B页面的时候,要返回到B的缓存页面,包括页面的基础数据和列表的滚动条位置信息 如果由B页面返回到A页面,则需要将B的缓存页清空上述另外一个场景:进入页面直接缓存,然后就结束了,这个比较简单本文就不讨论了3、在项目中的使用过程keepalive组件总共有三个参数include:可传字符串、正则表达式、数组,名称匹配成功的组件会被缓存exclude:可传字符串、正则表达式、数组,名称匹配成功的组件不会被缓存max:可传数字,限制缓存组件的最大数量,默认为10首先在App.vue根代码中添加引入keepalive组件,通过这里可以发现,我这里缓存的相当于整个页面,当然你也可以进行更细粒度的控制页面当中的某个区域组件<template> <router-view v-slot="{ Component }"> <keep-alive :include="keepAliveCache"> <component :is="Component" :key="$route.name" /> </keep-alive> </router-view> </template> <script lang="ts" setup> import { computed } from "vue"; import { useKeepAliverStore } from "@/store"; const useStore = useKeepAliverStore(); const keepAliveCache = computed(() => { return useStore.caches; }); </script>通过App.vue可以发现,通过pinia(也就是vue2中使用的vuex)保存要缓存的页面组件, 来处理include缓存,和保存页面组件中的滚动条信息数据import { defineStore } from "pinia"; export const useKeepAliverStore = defineStore("useKeepAliverStore", { state: () => ({ caches: [] as any, scrollList: new Map(), // 缓存页面组件如果又滚动条的高度 }), actions: { add(name: string) { this.caches.push(name); }, remove(name: string) { console.log(this.caches, 'this.caches') this.caches = this.caches.filter((item: any) => item !== name); console.log(this.caches, 'this.caches') }, clear() { this.caches = [] } } });组件路由刚刚切换时,通过beforeRouteEnter将组件写入include, 此时组件生命周期还没开始。如果都已经开始执行组件生命周期了,再写入就意义了。所以这个钩子函数就不能写在setup中,要单独提出来写。当然你也可以换成路由的其他钩子函数处理beforeEach,但这里面使用的话,好像使用不了pinia,这个还需要进一步研究一下。import { useRoute, useRouter, onBeforeRouteLeave } from "vue-router"; import { useKeepAliverStore } from "@/store"; const useStore = useKeepAliverStore() export default { name:"record-month", beforeRouteEnter(to, from, next) { next(vm => { if(from.name === 'Home' && to.name === 'record-month') { useStore.add(to.name) } }); } } </script>组件路由离开时判断,是否要移出缓存,这个钩子就直接写在setup中就可以了。onBeforeRouteLeave((to, from) => { console.log(to.name, "onBeforeRouteLeave"); if (to.name === "new-detection-detail") { console.log(to, from, "进入详情页面不做处理"); } else { useStore.remove(from.name) console.log(to, from, "删除组件缓存"); } });在keepalive两个钩子函数中进行处理scroll位置的缓存,onActivated中获取缓存中的位置, onDeactivated记录位置到缓存onActivated(() => { if(useStore.scrollList.get(routeName)) { const top = useStore.scrollList.get(routeName) refList.value.setScrollTop(Number(top)) } }); onDeactivated(() => { const top = refList.value.getScrollTop() useStore.scrollList.set(routeName, top) });这里定义一个方法,设置scrollTop使用了原生javascript的apiconst setScrollTop = (value: any) => { const dom = document.querySelector('.van-pull-refresh') dom!.scrollTop = value }同时高度怎么获取要先注册scroll事件,然后通过getScrollTop 获取当前滚动条的位置进行保存即可onMounted(() => { scrollDom.value = document.querySelector('.van-pull-refresh') as HTMLElement const throttledFun = useThrottleFn(() => { console.log(scrollDom.value?.scrollTop, 'addEventListener') state.scrollTop = scrollDom.value!.scrollTop }, 500) if(scrollDom.value) { scrollDom.value.addEventListener('scroll',throttledFun) } }) const getScrollTop = () => { console.log('scrollDom.vaue', scrollDom.value?.scrollTop) return state.scrollTop }上面注册scroll事件中使用了一个useThrottleFn,这个类库是@vueuse/core中提供的,其中封装了很多工具都非常不错,用兴趣的可以研究研究https://vueuse.org/shared/usethrottlefn/#usethrottlefn此时也可以查看找到实例的vnode查找到keepalive,是在keepalive紧挨着的子组件里const instance = getCurrentInstance() console.log(instance.vnode.parent) // 这里便是keepalive组件vnode // 如果是在开发环境中可以查看到cache对象 instance.vnode.parent.__v_cache // vue源码中,在dev环境对cache进行暴露,生产环境是看不到的 if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) { ;(instance as any).__v_cache = cache }4、vue3 keepalive源码调试1、克隆代码git clone git@github.com:vuejs/core.git2、安装依赖pnpm i3、如果不能使用pnpm,可以先通过npm安装一下npm i pnpm -g4、安装完成以后,找到根目录package.json文件中的scripts参考juejin.cn/post/699165…// 在dev命令后添加 --source-map是从已转换的代码,映射到原始的源文件 "dev": "node scripts/dev.js --sourcemap"5、执行pnpm run dev则会build vue源码pnpm run dev //则会出现以下,代表成功了(2022年5月27日),后期vue源代码作者可能会更新,相应的提示可能发生变更,请注意一下 > @3.2.36 dev H:\github\sourceCode\core > node scripts/dev.js --sourcemap watching: packages\vue\dist\vue.global.js //到..\..\core\packages\vue\dist便可以看到编译成功,以及可以查看到examples样例demo页面6、然后在 ....\core\packages\vue\examples\composition中添加一个aehyok.html文件,将如下代码进行拷贝,然后通过chrome浏览器打开,F12,找到源代码的Tab页面,通过快捷键Ctrl+ P 输入KeepAlive便可以找到这个组件,然后通过左侧行标右键就可以添加断点,进行调试,也可以通过右侧的【调用堆栈】进行快速跳转代码进行调试。<script src="../../dist/vue.global.js"></script> <script type="text/x-template" id="template-1"> <div>template-1</div> <div>template-1</div> </script> <script type="text/x-template" id="template-2"> <div>template-2</div> <div>template-2</div> </script> <script> const { reactive, computed } = Vue const Demo1 = { name: 'Demo1', template: '#template-1', setup(props) { } } const Demo2 = { name: 'Demo2', template: '#template-2', setup(props) { } } </script> <!-- App template (in DOM) --> <div id="demo"> <div>Hello World</div> <div>Hello World</div> <div>Hello World</div> <button @click="changeClick(1)">组件一</button> <button @click="changeClick(2)">组件二</button> <keep-alive :include="includeCache"> <component :is="componentCache" :key="componentName" v-if="componentName" /> </keep-alive> </div> <!-- App script --> <script> Vue.createApp({ components: { Demo1, Demo2 }, data: () => ({ includeCache: [], componentCache: '', componentName: '', }), methods:{ changeClick(type) { if(type === 1) { if(!this.includeCache.includes('Demo1')) { this.includeCache.push('Demo1') } console.log(this.includeCache, '000') this.componentCache = Demo1 this.componentName = 'Demo1' } if(type === 2) { if(!this.includeCache.includes('Demo2')) { this.includeCache.push('Demo2') } console.log(this.includeCache, '2222') this.componentName = 'Demo2' this.componentCache = Demo2 } } } }).mount('#demo') </script>7、调试源码发现 keepalive中的render函数(或者说时setup中的return 函数)在子组件切换时就会去执行,变更逻辑缓存第一次进入页面初始化keepalive组件会执行一次,然后点击组件一,再次执行render函数然后点击组件二,会再次执行render函数8、调试截图说明9、调试操作,小视频观看5、vue3 keealive源码粗浅分析通过查看vue3 KeepAlive.ts源码,源码路径:github.com/vuejs/core/…// 在setup初始化中,先获取keepalive实例 // getCurrentInstance() 可以获取当前组件的实例 const instance = getCurrentInstance()! // KeepAlive communicates with the instantiated renderer via the // ctx where the renderer passes in its internals, // and the KeepAlive instance exposes activate/deactivate implementations. // The whole point of this is to avoid importing KeepAlive directly in the // renderer to facilitate tree-shaking. const sharedContext = instance.ctx as KeepAliveContext // if the internal renderer is not registered, it indicates that this is server-side rendering, // for KeepAlive, we just need to render its children /// SSR 判断,暂时可以忽略掉即可。 if (__SSR__ && !sharedContext.renderer) { return () => { const children = slots.default && slots.default() return children && children.length === 1 ? children[0] : children } } // 通过Map存储缓存vnode, // 通过Set存储缓存的key(在外面设置的key,或者vnode的type) const cache: Cache = new Map() const keys: Keys = new Set() let current: VNode | null = null if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) { ;(instance as any).__v_cache = cache } const parentSuspense = instance.suspense const { renderer: { p: patch, m: move, um: _unmount, o: { createElement } } } = sharedContext // 创建了隐藏容器 const storageContainer = createElement('div') // 在实例上注册两个钩子函数 activate, deactivate sharedContext.activate = (vnode, container, anchor, isSVG, optimized) => { const instance = vnode.component! move(vnode, container, anchor, MoveType.ENTER, parentSuspense) // in case props have changed patch( instance.vnode, vnode, container, anchor, instance, parentSuspense, isSVG, vnode.slotScopeIds, optimized ) queuePostRenderEffect(() => { instance.isDeactivated = false if (instance.a) { invokeArrayFns(instance.a) } const vnodeHook = vnode.props && vnode.props.onVnodeMounted if (vnodeHook) { invokeVNodeHook(vnodeHook, instance.parent, vnode) } }, parentSuspense) if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) { // Update components tree devtoolsComponentAdded(instance) } } sharedContext.deactivate = (vnode: VNode) => { const instance = vnode.component! move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense) queuePostRenderEffect(() => { if (instance.da) { invokeArrayFns(instance.da) } const vnodeHook = vnode.props && vnode.props.onVnodeUnmounted if (vnodeHook) { invokeVNodeHook(vnodeHook, instance.parent, vnode) } instance.isDeactivated = true }, parentSuspense) if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) { // Update components tree devtoolsComponentAdded(instance) } } // 组件卸载 function unmount(vnode: VNode) { // reset the shapeFlag so it can be properly unmounted resetShapeFlag(vnode) _unmount(vnode, instance, parentSuspense, true) } // 定义 include和exclude变化时,对缓存进行动态处理 function pruneCache(filter?: (name: string) => boolean) { cache.forEach((vnode, key) => { const name = getComponentName(vnode.type as ConcreteComponent) if (name && (!filter || !filter(name))) { pruneCacheEntry(key) } }) } function pruneCacheEntry(key: CacheKey) { const cached = cache.get(key) as VNode if (!current || cached.type !== current.type) { unmount(cached) } else if (current) { // current active instance should no longer be kept-alive. // we can't unmount it now but it might be later, so reset its flag now. resetShapeFlag(current) } cache.delete(key) keys.delete(key) } // 可以发现通过include 可以配置被显示的组件, // 当然也可以设置exclude来配置不被显示的组件, // 组件切换时随时控制缓存 watch( () => [props.include, props.exclude], ([include, exclude]) => { include && pruneCache(name => matches(include, name)) exclude && pruneCache(name => !matches(exclude, name)) }, // prune post-render after `current` has been updated { flush: 'post', deep: true } ) // 定义当前组件Key // cache sub tree after render let pendingCacheKey: CacheKey | null = null // 这是一个重要的方法,设置缓存 const cacheSubtree = () => { // fix #1621, the pendingCacheKey could be 0 if (pendingCacheKey != null) { cache.set(pendingCacheKey, getInnerChild(instance.subTree)) } } onMounted(cacheSubtree) onUpdated(cacheSubtree) // 组件卸载的时候,对缓存列表进行循环判断处理 onBeforeUnmount(() => { cache.forEach(cached => { const { subTree, suspense } = instance const vnode = getInnerChild(subTree) if (cached.type === vnode.type) { // current instance will be unmounted as part of keep-alive's unmount resetShapeFlag(vnode) // but invoke its deactivated hook here const da = vnode.component!.da da && queuePostRenderEffect(da, suspense) return } unmount(cached) }) }) // 同时在keepAlive组件setup生命周期中,return () => {} 渲染的时候,对组件进行判断逻辑处理,同样对include和exclude判断渲染。 // 判断keepalive组件中的子组件,如果大于1个的话,直接警告处理了 // 另外如果渲染的不是虚拟dom(vNode),则直接返回渲染即可。 return () => { // eslint-disable-next-line no-debugger console.log(props.include, 'watch-include') pendingCacheKey = null if (!slots.default) { return null } const children = slots.default() const rawVNode = children[0] if (children.length > 1) { if (__DEV__) { warn(`KeepAlive should contain exactly one component child.`) } current = null return children } else if ( !isVNode(rawVNode) || (!(rawVNode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) && !(rawVNode.shapeFlag & ShapeFlags.SUSPENSE)) ) { current = null return rawVNode } // 接下来处理时Vnode虚拟dom的情况,先获取vnode let vnode = getInnerChild(rawVNode) // 节点类型 const comp = vnode.type as ConcreteComponent // for async components, name check should be based in its loaded // inner component if available // 获取组件名称 const name = getComponentName( isAsyncWrapper(vnode) ? (vnode.type as ComponentOptions).__asyncResolved || {} : comp ) //这个算是最熟悉的通过props传递进行的参数,进行解构 const { include, exclude, max } = props // include判断 组件名称如果没有设置, 或者组件名称不在include中, // exclude判断 组件名称有了,或者匹配了 // 对以上两种情况都不进行缓存处理,直接返回当前vnode虚拟dom即可。 if ( (include && (!name || !matches(include, name))) || (exclude && name && matches(exclude, name)) ) { current = vnode return rawVNode } // 接下来开始处理有缓存或者要缓存的了 // 先获取一下vnode的key设置,然后看看cache缓存中是否存在 const key = vnode.key == null ? comp : vnode.key const cachedVNode = cache.get(key) // 这一段可以忽略了,好像时ssContent相关,暂时不管了,没看明白?? // clone vnode if it's reused because we are going to mutate it if (vnode.el) { vnode = cloneVNode(vnode) if (rawVNode.shapeFlag & ShapeFlags.SUSPENSE) { rawVNode.ssContent = vnode } } // 上面判断了,如果没有设置key,则使用vNode的type作为key值 pendingCacheKey = key //判断上面缓存中是否存在vNode // if 存在的话,就将缓存中的vnode复制给当前的vnode // 同时还判断了组件是否为过渡组件 transition,如果是的话 需要注册过渡组件的钩子 // 同时先删除key,然后再重新添加key // else 不存在的话,就添加到缓存即可 // 并且要判断一下max最大缓存的数量是否超过了,超过了,则通过淘汰LPR算法,删除最旧的一个缓存 // 最后又判断了一下是否为Suspense。也是vue3新增的高阶组件。 if (cachedVNode) { // copy over mounted state vnode.el = cachedVNode.el vnode.component = cachedVNode.component if (vnode.transition) { // recursively update transition hooks on subTree setTransitionHooks(vnode, vnode.transition!) } // avoid vnode being mounted as fresh vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE // make this key the freshest keys.delete(key) keys.add(key) } else { keys.add(key) // prune oldest entry if (max && keys.size > parseInt(max as string, 10)) { pruneCacheEntry(keys.values().next().value) } } // avoid vnode being unmounted vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE current = vnode return isSuspense(rawVNode.type) ? rawVNode : vnode6、总结通过这次查看vue3 keepalive源码发现,其实也没那么难,当然这次查看源代码也只是粗略查看,并没有看的那么细,主要还是先解决问题。动动手调试一下,有时候真的就是不逼一下自己都不知道自己有多么的优秀。原来我也能稍微看看源代码了。以后有空可以多看看vue3源代码,学习一下vue3的精髓。了解vue3更为细节的一些知识点。github.com/aehyok/vue-…本文涉及到的代码后续会整理到该代码仓库中github.com/aehyok/2022最后自己每天工作中的笔记记录仓库,主要以文章链接和问题处理方案为主。
大家好,我是aehyok🎋,一个住在深圳城市的佛系码农🧚🏻♀️,如果你喜欢我的文章📚,可以通过点赞帮我聚集灵力⭐️。个人github仓库地址: https:github.com/aehyok本文讲解代码仓库地址 : https:github.com/aehyok/vue-… 目前基于dev分支进行开发和测试本demo已部署腾讯云 vue.tuokecat.com(服务器配置较低,如有访问比较慢,请耐心等待)table封装路径为根路径下的 vue-qiankun/common/components/form/form表单json配置生成器1、 在PC端日常的使用中,使用最多的莫过于表单和列表了,故此对table列表和form表单进行了统一的封装,通过json配置就可以快速适配table列表和form表单。2、本章节主要记录自己的form表单封装3、封装思路A、根据布局,一行一列默认可不设置(columnSpan设置为24),一行两列可设置参数columnSpan设置为12,后续以此类推B、根据不同的字段类型,分别对应子组件进行渲染C、子组件根据不同的类型,以及配置的类型字段进行渲染和数据绑定D、子组件可以设置必填项和rules表单验证规则E、可以通过设置字段的值,去控制其他字段的展示和隐藏F、下拉等字典类型数据,可统一设置读取接口数据,也可以根据需要进行传递当前数组数据G、图片上传可设置上传接口,并可设置上传多张图片H、富文本编辑器也可以作为组件嵌入表单I、 ......先来一个完整的效果展示1、form表单配置json{ "formListItem": [ { "name": "name1", "type": "text", "title": "栏目标题", "required": true }, { "name": "name", "type": "text", "title": "栏目名称" }, { "name": "total", "type": "number", "title": "栏目数量", "required": true }, { "name": "count", "type": "number", "title": "浏览数量" }, { "name": "descript", "type": "textarea", "title": "备注", "required": true, "rows": 3 }, { "name": "content", "type": "textarea", "title": "内容", "rows": 3 }, { "name": "startDate", "type": "date", "title": "开始日期", "required": true }, { "name": "endDate", "type": "date", "title": "结束日期" }, { "name": "isValid", "type": "switch", "title": "是否有效" }, { "name": "isExpired", "type": "switch", "title": "是否过期", "required": true }, { "name": "type", "type": "radio", "dictionary": [ { "code": 1, "name": "横版栏目" }, { "code": 2, "name": "竖版栏目" } ], "title": "栏目类型", "controls": [ { "value": 1, "showCondition": [ { "name": "show", "type": "radio", "dictionary": [ { "code": 1, "name": "China" }, { "code": 2, "name": "English" } ], "title": "测试类型", "required": true }, { "name": "image1", "type": "ImageTypeView", "title": "文件" } ] }, { "value": 2, "showCondition": [ { "name": "isValids", "type": "switch", "title": "是否有效" } ] } ] }, { "name": "requireType", "type": "radio", "dictionary": [ { "code": 1, "name": "类型一" }, { "code": 2, "name": "类型二" } ], "title": "图文类型", "required": true }, { "name": "range", "type": "checkbox", "title": "发布范围", "dictionary": [ { "code": 1, "name": "范围一" }, { "code": 2, "name": "范围二" } ], "required": true }, { "name": "dateRange", "type": "daterange", "title": "日期范围" }, { "name": "creType", "type": "select", "dictionary": [ { "code": 1, "name": "身份证" }, { "code": 2, "name": "居住证" } ], "title": "证件类型" }, { "name": "image", "type": "image", "title": "头像" } ], "formData": { "name": "主菜单栏目", "total": null, "count": null, "createDate": 1606730360386, "type": 1, "creType": "", "range": [], "isExpired": false, "isValid": true } }2、 最后的效果图片效果展示的在线预览页面为 vue.tuokecat.com/#/webpack-a…具体代码可根据路由进行搜索字段配置详细介绍1、静态文本 static```javascript { type: "static", // 字段类型只读文本 name: "name", //与后台对接字段 title: "名称", // 前端展示字段 }, ```2、文本框 text```javascript { type: "text", // 字段类型文本框 name: "name", //与后台对接字段 title: "域名", // 前端展示字段 required: true, // 必填项设置 maxlength: 50, // 字符串长度限制 showwordlimit: true, // 是否显示字符串长度 placeholder:"请输入10个字符以内的名称", // 占位文本提示 append: ".com", // 文本框后置内容 // rules // 数组 rules: [ { pattern: /(^[1-9]([0-9]+)?(\.[0-9]{1,2})?$)|(^(0){1}$)|(^[0-9]\.[0-9]([0-9])?$)/, message: '请输入数字最多两位小数' } ], }, ```3、文本域 textarea```javascript { type: "textarea", // 字段类型文本域 name: "name", //与后台对接字段 title: "备注", // 前端展示字段 required: true, // 必填项设置 placeholder:"请输入10个字符以内的名称", // 占位文本提示 rows: 4, // 输入框行数 minlength: 100, // 最小输入长度 maxlength: 5000, // 最大输入长度 showwordlimit: true, // 是否显示字符串长度 // rules // 数组 rules: [ { pattern: /(^[1-9]([0-9]+)?(\.[0-9]{1,2})?$)|(^(0){1}$)|(^[0-9]\.[0-9]([0-9])?$)/, message: '请输入数字最多两位小数' } ], }, ```4、下拉框 select```javascript { type: "select", // 字段类型下拉框 name: "options", //与后台对接字段 title: "类型", // 前端展示字段 required: true, // 必填项设置 placeholder:"请选择类型", // 占位文本提示 // dictionary 可直接传递下拉数据,也可以传递字典中的typeCode,通过内部接口获取 dictionary: 7010, // 7010为字典中维护的typecode dictionary:[ { code: 1, name:"图片", }, { code: 2, name:"视频" } ], multiple: true, // 下拉列表可以多选 // rules // 正则匹配 rules: [ { pattern: /(^[1-9]([0-9]+)?(\.[0-9]{1,2})?$)|(^(0){1}$)|(^[0-9]\.[0-9]([0-9])?$)/, message: '只能选择*******' } ], // 点击下来触发切换联动的事件,为一个函数 changeFunction: function(){} }, ```5、富文本 editor```javascript { type: "editor", // 字段类型富文本 name: "content", //与后台对接字段 title: "内容", // 前端展示字段 required: true, // 必填项设置 placeholder:"请选择类型", // 占位文本提示 // rules // 正则匹配 rules: [ { pattern: /(^[1-9]([0-9]+)?(\.[0-9]{1,2})?$)|(^(0){1}$)|(^[0-9]\.[0-9]([0-9])?$)/, message: '只能选择*******' } ], maxLength:5000, // 富文本框最大长度,默认5000 }, ```6、数值框 number```javascript { type: "number", // 字段类型数值 name: "num", //与后台对接字段 title: "总数", // 前端展示字段 required: true, // 必填项设置 placeholder:"请输入10个字符以内的名称", // 占位文本提示 precision: 2, // 小数点后的位数 // rules // 数组 rules: [ { pattern: /(^[1-9]([0-9]+)?(\.[0-9]{1,2})?$)|(^(0){1}$)|(^[0-9]\.[0-9]([0-9])?$)/, message: '请输入数字最多两位小数' } ], }, ```7、省市区三级级联选择 citySelect```javascript { type: "citySelect", // 字段类型省市区 name: "region", //与后台对接字段 title: "户籍地", // 前端展示字段 required: true, // 必填项设置 placeholder:"请输入10个字符以内的名称", // 占位文本提示 // rules // 数组 rules: [ { pattern: /(^[1-9]([0-9]+)?(\.[0-9]{1,2})?$)|(^(0){1}$)|(^[0-9]\.[0-9]([0-9])?$)/, message: '请输入数字最多两位小数' } ], }, ``` 8、 图片上传 image```javascript { type: "image", // 字段类型图片 name: "images", //与后台对接字段 title: "上传图片", // 前端展示字段 required: true, // 必填项设置 placeholder:"请上传图片", // 占位文本提示 // rules // 数组 rules: [ { pattern: /(^[1-9]([0-9]+)?(\.[0-9]{1,2})?$)|(^(0){1}$)|(^[0-9]\.[0-9]([0-9])?$)/, message: '请输入数字最多两位小数' } ], }, ``` 9、 视频上传 video```javascript { type: "video", // 字段类型视频 name: "images", //与后台对接字段 title: "上传视频", // 前端展示字段 required: true, // 必填项设置 placeholder:"请上传视频", // 占位文本提示 // rules // 数组 rules: [ { pattern: /(^[1-9]([0-9]+)?(\.[0-9]{1,2})?$)|(^(0){1}$)|(^[0-9]\.[0-9]([0-9])?$)/, message: '请输入数字最多两位小数' } ], }, ``` 10、 日期 date```javascript { type: "date", // 字段类型日期 name: "date", //与后台对接字段 title: "日期", // 前端展示字段 required: true, // 必填项设置 placeholder:"请选择日期", // 占位文本提示 // rules // 数组 rules: [ { pattern: /(^[1-9]([0-9]+)?(\.[0-9]{1,2})?$)|(^(0){1}$)|(^[0-9]\.[0-9]([0-9])?$)/, message: '请输入数字最多两位小数' } ], }, ``` 11、 日期范围 daterange```javascript { type: "daterange", // 字段类型日期 name: "date", //与后台对接字段 title: "日期范围", // 前端展示字段 required: true, // 必填项设置 placeholder:"请选择日期", // 占位文本提示 // rules // 数组 rules: [ { pattern: /(^[1-9]([0-9]+)?(\.[0-9]{1,2})?$)|(^(0){1}$)|(^[0-9]\.[0-9]([0-9])?$)/, message: '请输入数字最多两位小数' } ], }, ``` 附上整个调用的所有代码1、template 模板代码<template> <div> <sl-table :list="list" @handleSelectionChange="handleSelectionChange" :columns="columns" :operates="operates" v-model:pageModel="pageModel" @search="search" > </sl-table> </div> </template>2、'script'标签代码import SlTable from '../../../common/components/table/index.vue' import { defineComponent, reactive, toRefs } from "vue"; import { list_test, columns_test } from "./tableConfig"; export default defineComponent({ components: { SlTable }, setup() { // 选中行 const handleSelectionChange = (val) => { console.log("handleSelectionChange-val:", val); }; // 编辑 const handleDetail = (index, row, idx) => { console.log("index:", index, idx); console.log("row:", row); }; // 删除 const handleDel = (index, row) => { console.log(" index:", index); console.log(" row:", row); }; const state = reactive({ pageModel: { page: 1, limit: 10, total: 17 }, list: [], // table数据 columns: [], // 需要展示的列 operates: { width: 200, fixed: "right", list: [ { id: "1", label: "查看", type: "text", show: true, disabled: false, method: (index, row, ss) => { handleDetail(index, row, ss); } }, { id: "2", label: "删除", type: "text", show: true, disabled: false, method: (index, row) => { handleDel(index, row); } } ] } // 列操作按钮 }); state.list = list_test; state.columns = columns_test; const search = () => { state.list = [...state.list]; console.log(state.pageModel, "state.pageModel"); }; return { ...toRefs(state), handleSelectionChange, search }; } });3、其中模拟数据和字段配置在单独的文件中tableConfigconst list_test = [ { id: "24", title: "编号3", state: 0, createTime:"2021-09-23T17:57:09", remark: "自定义" }, { id: "23", title: "编号4", state: 1, createTime:"2021-09-23T17:57:19", remark: "自定义" }, { id: "23", title: "编号5", state: 2, createTime:"2021-09-23T17:57:29", remark: "自定义" }, { id: "23", title: "编号5", state: 1, createTime:"2021-09-23T17:57:39", remark: "自定义111" }, { id: "223", title: "编号3", state: 1, createTime:"2021-09-23T17:57:49", remark: "22222" }, { id: "2444", title: "编号3", state: 0, createTime:"2021-09-23T17:57:59", remark: "333333" }, { id: "24", title: "编号3", state: 0, createTime:"2021-09-23T17:57:09", remark: "自定义" }, { id: "23", title: "编号4", state: 1, createTime:"2021-09-23T17:57:19", remark: "自定义" }, { id: "23", title: "编号5", state: 2, createTime:"2021-09-23T17:57:29", remark: "自定义" }, { id: "23", title: "编号5", state: 1, createTime:"2021-09-23T17:57:39", remark: "自定义111" }, { id: "223", title: "编号3", state: 1, createTime:"2021-09-23T17:57:49", remark: "22222" }, { id: "2444", title: "编号3", state: 0, createTime:"2021-09-23T17:57:59", remark: "333333" } ] const columns_test = [ { type:'checkbox', }, { prop: "id", label: "编号", type:'index', align: "center" }, { prop: "title", label: "标题", align: "center", }, { prop: "createTime", label: "创建时间", align: "center", dateFormat: "yyyy-MM-dd HH:mm:ss", sortable: true }, { prop: "state", label: "状态", align: "center", dictionary: [ { code: 0, name: "待审核"}, { code: 1, name: "已审核"}, { code: 2, name: "审核中"}, ] }, { prop:"custom", label:"自定义", align: "center", html: (row, column) => { return row.title==="编号3" ? `<span style="color: red;">${ row.remark }</span>`:`未定义` } } ] export { list_test, columns_test }最后的最后github.com/aehyok/vue-…本文中不涉及到代码,有关代码问题可以访问文章开头的微前端github demo 仓库,github仓库将会保持持续更新,不断优化小demo。github.com/aehyok/2021最后自己每天工作中的笔记记录仓库,主要以文章链接和问题处理方案为主。
大家好,我是aehyok🎋,一个住在深圳城市的佛系码农🧚🏻♀️,如果你喜欢我的文章📚,可以通过点赞帮我聚集灵力⭐️。个人github仓库地址: https:github.com/aehyok本文讲解代码仓库地址 : https:github.com/aehyok/vue-… 目前基于dev分支进行开发和测试本demo已部署腾讯云 vue.tuokecat.com(服务器配置较低,如有访问比较慢,请耐心等待)table列表json配置生成器1、 在PC端日常的使用中,使用最多的过于表单和列表了,故此对table列表和form表单进行了统一的封装,通过json配置就可以快速适配table列表和form表单。2、封装思路A、列表的搜索条件和搜索按钮,这个与table 可以解耦,目前放到单独的组件中,所以本节暂不考虑B、table列表显示字段,根据不同的类型进行制定C、最右侧的操作按钮的配置,比如(行编辑、删除等操作),根据定义的函数进行注入,后面实现函数操作进行自定义,不与table列表冲突D、特殊的字段,比如(序号字段、多选框、单选框等等)E、最后当然少不了分页器的参与3、本章节主要记录自己的table封装先来一个完整的效果展示1、列表展示字段的配置json{ type:'checkbox', }, { type:'index', }, { prop: "title", label: "标题", align: "center", }, { prop: "createTime", label: "创建时间", align: "center", dateFormat: "yyyy-MM-dd HH:mm:ss", sortable: true }, { prop: "state", label: "状态", align: "center", dictionary: [ { code: 0, name: "待审核"}, { code: 1, name: "已审核"}, { code: 2, name: "审核中"}, ] }, { prop:"custom", label:"自定义", align: "center", html: (row, column) => { return row.title==="编号3" ? `<span style="color: red;">${ row.remark }</span>`:`未定义` } }最后一个字段 custom 可以通过配置html,自定义展示复杂的组件和样式介入2、右侧操作按钮的配置信息{ width: 200, fixed: "right", list: [ { id: "1", label: "查看", type: "text", show: true, disabled: false, method: (index, row, ss) => { handleDetail(index, row, ss); } }, { id: "2", label: "删除", type: "text", show: true, disabled: false, method: (index, row) => { handleDel(index, row); } } ] } 其中的handleDetail和handleDel函数通过自定义实现业务对接即可。3、 最后的效果图片字段配置详细介绍1、普通字段直接配置```javascript { prop: "name", label: "设施名称", align: "center", } ```2、序号字段配置(后期可直接配置自定义序号,暂时从 1 自增+1)```javascript { type: "index" } ```3、checkbox 字段配置(后期可添加单选框的配置)```javascript { type: "checkbox" } ```4、日期格式字段配置,可设置转换格式```javascript { prop: "recorDate", label: "返乡日期", align: "center", dateFormat: "yyyy-MM-dd" }, ```5、字典数据转换```javascript { prop: "sex", label: "性别", align: "center", dictionary:[ { code: 1, name:'男', }, { code: 2, name:'女', } ] }, ```6、自定义字段展示内容```javascript { prop: "", label: "自定义", align: "center", html: (row, column) => { return row.name==="集资球场" || row.address ==="22" ? `<span style="color: red;">${ row.address }</span>`:`222` } }, ```7、显示 image```javascript { prop: "image", label: "自定义", align: "center", image:'image' }, ```8、设置字段排序(前端自动排序)```javascript { prop: "image", label: "自定义", align: "center", sortable: true }, ```9、设置字段自定义调用接口排序```javascript { prop: "image", label: "自定义", align: "center", sortable: "custom", // 通过传递的search查询函数中添加了orders排序字段 } ```10、其他字段待补充......最后的最后github.com/aehyok/vue-…本文中不涉及到封装的组件代码,有关代码问题可以访问文章开头的微前端github demo 仓库,github仓库将会保持持续更新,不断优化小demo。github.com/aehyok/2021最后自己每天工作中的笔记记录仓库,主要以文章链接和问题处理方案为主。
我的博客即将入驻“云栖社区”,诚邀技术同仁一同入驻。
本文个人同步博客地址:http://aehyok.com/Blog/Detail/78.html 个人网站地址:aehyok.com QQ 技术群号:206058845,验证码为:aehyok 本文文章链接:http://www.cnblogs.com/aehyok/p/3981965.html 感谢您的阅读,如果您对我的博客所讲述的内容有兴趣,那不妨点个推荐吧,谢谢支持:-O。
前言 本文个人同步博客地址http://aehyok.com/Blog/Detail/76.html 个人网站地址:aehyok.com QQ 技术群号:206058845,验证码为:aehyok 本文文章链接:http://www.cnblogs.com/aehyok/p/3946286.html 感谢您的阅读,如果您对我的博客所讲述的内容有兴趣,那不妨点个推荐吧,谢谢支持:-O。
前言 本人个人博客原文链接地址为http://aehyok.com/Blog/Detail/75.html。 个人网站地址:aehyok.com QQ 技术群号:206058845,验证码为:aehyok 本文文章链接:http://www.cnblogs.com/aehyok/p/3946286.html 感谢您的阅读,如果您对我的博客所讲述的内容有兴趣,那不妨点个推荐吧,谢谢支持:-O。
本人个人博客原文链接地址为http://aehyok.com/Blog/Detail/66.html。 个人网站地址:aehyok.com QQ 技术群号:206058845,验证码为:aehyok 本文文章链接:http://www.cnblogs.com/aehyok/p/3946286.html 感谢您的阅读,如果您对我的博客所讲述的内容有兴趣,那不妨点个推荐吧,谢谢支持:-O。
个人同步本文博客地址http://aehyok.com/Blog/Detail/64.html 个人网站地址:aehyok.com QQ 技术群号:206058845,验证码为:aehyok 本文文章链接:http://aehyok.com/Blog/Detail/64.html 感谢您的阅读,如果您对我的博客所讲述的内容有兴趣,那不妨点个推荐吧,谢谢支持:-O。
1、打开Setup进行安装 2、下一步,然后功能全选 3、点击安装,便开始安装了 安装成功 配置 进行配置之后,选择高级,因为其他功能可能没那么多 到如下界面后,直接进行下一步就可以 下一步,设置TFS的主账户,还是使用之前已经准备好的账户tfsservice 继续下一步下一步,到报表读者帐户,填写之前准备好的账户tfsreports 继续下一步,到这里进行添加Windows SharePoint Services的帐号,同样还是之前准备好的 继续下一步,到如下图所示,现在我这里报错了,可能需要重启电脑,那我就重启一下试试看吧。 重启电脑后,然后继续按照上面的过程重新配置一遍即可。如下图所示,重启一下电脑还真的可以了。 然后点击配置按钮,之后就会进行配置安装了 等一段时间之后,就配置安装完成 接下来是Power Tools的安装,点击安装下一步就可以了,比较简单。
前言 SQLite 是一个软件库,实现了自给自足的、无服务器的、零配置的、事务性的 SQL 数据库引擎。SQLite 是在世界上最广泛部署的 SQL 数据库引擎。SQLite 源代码不受版权限制。 简单的认识了SQLite之后,我就很想来尝试一下,他如此的轻量,作为一个程序员,我没有理由不去学习一下。 SQLite下载和基础使用 1、下载暂时只看到32位的,下载地址http://www.sqlite.org/download.html 2、下载后解压,然后将exe文件复制到C:\Sqlite 3、打开运行窗口,输入CMD 输入CD\返回到C盘根目录 再输入 CD Sqlite,会跳转到C:\Sqlite文件夹下(如果你想去D盘,那么就输入D:就可以了) 4、现在开始创建数据库:调用C:\Sqlite下的sqlite3.exe文件,输入命令为 sqlite3 C:\Sqlite\aehyok.db 现在可以看到SQLite的版本号了,同时我们来看看C:\Sqlite下的文件 5、再输入一次 sqlite3 C:\Sqlite\aehyok.db;虽然命令报错了,但是数据库还是生成了,不知道为什么,现在还可以用,那就继续吧 6、来创建一张数据库表 输入命令create table Test(id integer primary key,name text); 再来插入两条数据 输入命令 insert into Test(id,name) values(1,'aehyok'); insert into Test(id,name) values(2,'Candy'); 7、现在再来查询一下 select * from Test; 8、接下来试试修改数据吧 update Test set name='aehyoks' where id=1; 9、最后再来来删除勒 delete from Test where id=1; 10、改换显示模式 11、创建视图 12、创建索引 13、显示表结构 14、显示表和视图 15、获取指定表的索引 16、导出数据到Sql文件 17、从Sql文件中导入数据库 18、列出当前数据文件中的数据库 19、备份数据库 20、恢复数据或数据库 1代表数据已被清空 2代表恢复数据或者数据库 可以下面三个error 其实就是恢复数据结构的过程 3数据已经恢复又可以查出原来的数据了 总结 简单的试了一下一些基础的SQL指令,感觉还是比较简单的,这也可以作为App数据存储开发的利器呀。好东西,打算在Python的学习中来使用这款超级轻量好用的SQLite数据库。 个人网站地址:aehyok.com QQ 技术群号:206058845,验证码为:aehyok 本文文章链接:http://www.cnblogs.com/aehyok/p/3981965.html 感谢您的阅读,如果您对我的博客所讲述的内容有兴趣,那不妨点个推荐吧,谢谢支持:-O。
前言 在上一节中简单的介绍了在VS2013中如何进行开发Hello World,在VS2013中进行搭建了环境http://www.cnblogs.com/aehyok/p/3986168.html。本节主要来简单的学习一下关于Python的基础。 Python基础入门 1、打印一个字符串Hello World. print('Hello World') 2、打印一个路径 print('C:\aehyok\aehyok') 可以发现\a发生了转义。如果不想发生转义,只需要在字符串前添加一个r print(r'C:\aehyok\aehyok') 3、字符串拼接和字符串重复次数 print('str'+'ing','aehyok'*3) 4、字符串两种索引方式,从左到右索引从0开始,从右到左索引从-1开始。 word='aehyok' print(word[0],word[5]) print(word[-1],word[-5]) 5、对字符串进行切分:用冒号分隔两个索引,形式为变量[头下标:尾下标]。截取的范围是前闭后开的 cat='aehyok' print(cat[3:]) print(cat[3:6]) print(cat[:-1]) print(cat[-1]) 6、获取键入字符串,并打印字符串以及字符串长度和类型 object = input('Enter an ojbect : ') print("Get anlu integer is",object,type(object),len(object))
前言 首先奉上个人网站地址传送门:aehyok.com。 aehyok.com的成长之路一——开篇 中主要阐述了自己为什么建立自己的网站,以及个人网站的大致方向。 aehyok.com的成长之路二——技术选型 中主要简单概括了自己搭建网站过程中使用或者以后可能使用的技术。 本篇博客主要简单的来介绍一下,自己目前网站的架构,以及自己的一些想法。当然这其中很大程度受益于大神@郭明峰的开源框架以及他的指点。 本人最近也建立了一个QQ技术群,本人个人网站所涉及的所有源码也将会在群内共享。欢迎各位喜欢学习技术的朋友们入住。 群账号为:206058845,记住群验证码为:aehyok。 框架项目结构简要说明 1-Infrastructure(基础设施) 1、aehyok.Utility:通用技术工具类 说明: 分类封装通用的与技术无关的辅助工具类功能 依赖项:无 2、aehyok.Model:各种实体类的定义 说明:1、业务实体模型 2、数据传输模型 3、展现视图模型 依赖项:无 3、 aehyok.Core:aehyok框架核心组件 说明: 1. 定义aehyok框架的核心,是整个框架运行的骨架。 2. 该骨架提供数据存储、日志、缓存、权限等模块的基础接口或基类,不提供具体实现。 3. 业务层依赖于此层的接口及基类进行业务操作,而不依赖于具体实现。 4. 此层定义的接口与基类的具体实现,都是可替换的。以适应不同的业务对基础模块功能的需求。 依赖项:aehyok.Utility 4、aehyok.Core.Data.Entity:EntityFramework数据存储组件 说明: 1.提供aehyok.Core中定义的数据存储功能的EntityFramework的实现方案 2.数据库初始化策略 3.业务实体类映射 依赖项: 1.aehyok.Utility 2.aehyok.Model 3.aehyok.Core 4.EntityFramework 2-Application(应用的接口和对应的实现) 5、aehyok.Contracts:服务契约层 说明: 包含业务功能的接口的定义 依赖项: 1. aehyok.Utility 2. aehyok.Core 3.aehyok.Model 6、 aehyok.Services:服务实现层 说明: 包含数据功能初始化及服务业务功能实现 依赖项:1. aehyok.Utility 2. aehyok.Model 3. aehyok.Core 4. aehyok.Core.Data.Entity 5. aehyok.Contracts 3-Presentation(各种可视化UI展现层) 7、aehyok.WebMvc和aehyok.Admin.Mvc:前后台Web 展现层 说明: 前后台UI展现层 依赖项:1.aehyok.Utility 2.aehyok.Model 3.aehyok.Core 4.aehyok.Core.Data.Entity 5.aehyok.Contracts 6.aehyok.Services 4-UnitTest 8、aehyok.UnitTest:单元测试 说明: 单元测试还在学习摸索中 依赖项:各种可依赖。 简要说明: 1、面向接口的方式来开发,然后通过IOC来接触模块之间的耦合。 2、aehyok.Core:提供数据存储、日志、缓存、权限等模块的基础接口或基类。 3、aehyok.Core.Data.Entity:EntityFramework数据存储组件,很明显该层是实现aehyok.Core中的数据存储组件的接口和基类。为什么这样设计呢?这样就可以很好的被替换掉,比如你现在的项目用的是EntityFramework开发的,项目经理突然说EntityFramework有这样的的问题满足不了我们系统,现在决定使用NHibernate,那么现在就只需要将该层进行研究实现,进行IOC注入就可以了,其他层的任何代码都不需要进行修改(自己想的暂时是这样的,不知道实现起来难度到底有多大)。 4、aehyok.Core.Logging.Log4Net:Log4Net日志组件,该层也是实现aehyok.Core中日志组件的接口和基类。同样可以很好的替换掉。可以自己实现,也可以采用其他日志组件NLog等等吧,都是可以的。 5、可能还有会Cache组件、权限组件,当然可能还会有其他的……按道理都是可以被替换的。 6、aehyok.Contracts和aehyok.Services可以说是业务逻辑层吧。实现中调用各组件进行拼装完成需要的方法。供UI层进行调用即可。 7、当然对于UI层的展现考虑最好也是可以被替换的。打个比方:现在后台用的是Bootstrap模版进行开发的,然后突然有个公司觉得这框架很不错,想来买你们的源码,但是他们对Bootstrap不熟悉,或者不想用Bootstrap,想用其他的前端UI框架。不清楚通过MVVM能否实现前端UI的良好解耦。 8、可能还有其他未知的问题吧,暂时考虑的就这么多,还要在以后的实践中不断的思考不断的来完善吧,计划没有变化快,但没有计划怎么来的变化呢? 总结 自己的小站还在持续不断的更新的,虽然更新速度如蜗牛般缓慢,但是网站还是在一天天的进步,自己也在一点点的积累。以上也只在于技术的学习,而不管其项目大小、技术学习成本等现实因素关联起来,其实我就是为了学习技术,让自己能够进步成长罢了。 本人最近也建立了一个QQ技术群,现在里面人数不是很多,是在写上一篇博客的时候开始加人的。由于本人最近在使用TFS OnLine,看到他对于团队开发项目来说绝对是利器,想多多的学习了解一下。 本人个人网站所涉及的所有源码也将会在群内共享。群账号为:206058845,记住群验证码为:aehyok。欢迎各位喜欢学习技术的朋友们入住。。期待我们的共同成长。同时希望自己能够一直坚持学习下去。 最后再次奉上个人网站地址传送门:aehyok.com 个人网站地址:aehyok.com QQ 技术群号:206058845,验证码为:aehyok 本文文章链接:http://www.cnblogs.com/aehyok/p/3946286.html 感谢您的阅读,如果您对我的博客所讲述的内容有兴趣,那不妨点个推荐吧,谢谢支持:-O。
前言 Python是一个高层次的结合了解释性、编译性、互动性和面向对象的脚本语言。 Python的设计具有很强的可读性,相比其他语言经常使用英文关键字,其他语言的一些标点符号,它具有比其他语言更有特色语法结构。 Python 是一种解释型语言: 这意味着开发过程中没有了编译这个环节。类似于PHP和Perl语言。 Python 是交互式语言: 这意味着,您可以在一个Python提示符,直接互动执行写你的程序。 Python 是面向对象语言: 这意味着Python支持面向对象的风格或代码封装在对象的编程技术。 Python是初学者的语言:Python 对初级程序员而言,是一种伟大的语言,它支持广泛的应用程序开发,从简单的文字处理到 WWW 浏览器再到游戏。 自己也只搞过.NET吧,想着开拓一下自己的视野,于是选择了这个脚本语言来学习一下。每次来研究一点点做个笔记,以便于自己之后来查看,也希望对更多想学习的人能有点帮助吧。 环境准备 Win7 64位搭建开发环境。需要准备VS2013、Python、PTVS2013。 1、http://pytools.codeplex.com/ 下载工具,下载之后进行安装即可,我这里下载的是。 2、https://www.python.org/download/下载Python 3、打开VS2013,新建项目 确定建立项目 Hello World运行成功 总结 学习这门语言主要是为了开拓自己的视野。这也算是除了.NET之外学习的第一门语言吧。算是一个全新的开始,用VS这个宇宙间最强大的编辑器来学习,体会其中的快乐。
1、首先要安装一个工具Power Tools,点击安装,然后下一步就可以了。 2、安装完之后重新打开TFS管理控制台 点击Create Backup Plan 点击下一步,它会导航到第一个页面,在这个页面中你可以输入备份的路径和你希望保存备份的天数(超过这个天数以后,它会自动地删除备份)。 点击下一步进行设置一个密码 下一步设置一个用户名和密码 继续下一步 继续下一步 发现问题,那么就来解决一下吧,先来设置一个上面那个共享文件夹 再在服务器上找到对应位置 重启服务即可。 现在返回到TFS管理控制台,点击重新检查 重新检查后,全部通过 然后点击 Create Plan,等待后就会完成了。 进行一次完整的手动备份 OK,备份完成。 生成的备份文件
最近刚刚搭建好服务器,然后准备将VSS源代码迁移到TFS源代码管理服务器上面。在我本机先用的服务器帐号来上传初始化源代码数据库,然后我又用自己的帐号进行迁出代码的时候发生的异常。 造成上述错误,主要是因为我在本机先用一个帐号进行源代码初始化过程,而后我又用另外一个帐号来管理代码。 这样就导致了上述问题。 第一种解决方案便是:在针对两个帐号建立两个映射文件夹即可解决,各用各的就可以了。 第二种解决方案便是:如果你想让两个帐号用同一个文件夹下的。 打开VS2010 打开工作区 点击编辑 设置好权限和工作文件即不会出现这样的问题了。
前言 http://www.cnblogs.com/aehyok/p/3979707.html 这里简单介绍了安装windows Server 2008 R2系统,接下来就开始介绍安装Team Foundation Server 2010工具之前的准备工作。 1、准备好TFS需要使用到的用户,在如下图的位置进行添加即可。 2、IIS的安装。 3、可能还需要安装.Net Framework 3.5(在此就不进行过多的介绍了,下载安装即可) 4、SQL Server 2008 R2的安装 下一篇主要来处理TFS的安装和配置篇。 1、TFS使用的系统用户的准备 1、准备好TFS需要使用到的用户,在如下图的位置进行添加即可。 2、给tfsreports设置允许本地登录的授权 3、给tfsservice设置作为服务登录 2、IIS安装 IIS安装成功。 3、SQL Server 2008 R2的安装 1、安装SQL Server 2008 R2 一路下一步到下图所示 下一步直接全选功能,防止有些功能使用异常。 继续下一步下一步到达如下图所示 下一步 添加当前系统管理员用户,和上面准备好的tfsservice和sqlservice 同理要添加当前系统管理员用户,和上面准备好的tfsservice和sqlservice 本机模式默认配置 下一步然后到此界面算是开始安装了 安装成功 总结 安装比较顺利,没出现大状况,接下来就要来安装TFS了,然后对其进行配置使用。
1、安装Backup 2、打开Backup工具 3、一次性备份 下一步
1、http://msdn.itellyou.cn/ 在此下载IOS文件。 2、通过Nero进行刻录系统光盘,可以通过Daemon直接加载IOS,然后复制就可以了。 3、通过开机 Delete键进BIOS设置从光盘启动进行安装系统。 4、安装完系统后可以通过工具进行激活系统,工具可直接百度就可以了。 5、安装杀毒软件。 6、安装VMware虚机。 7、VSS2005管理工具。
---------------------------本地用户和组---------------------------在计算机 WINSERVER2008R2 上创建用户 lintx 时,出现了以下错误: 密码不满足密码策略的要求。检查最小密码长度、密码复杂性和密码历史的要求。---------------------------确定 --------------------------- 主要是修改以下两个策略 1、密码必须符合复杂性要求——禁用 2、密码长度最小值——设置为0
1、点桌面任务栏的“开始-->运行”在弹出的窗口中输入gpedit.msc 。 2、找到如下图所示的位置 右键属性进行设置如下
1、在windows Server 2008 R2上访问百度,会出现以下界面 当在Windows Sever 2008 R2中运动IE8的时候会发现默认情况下IE启用了增强的安全配置,为了方便而且是在内网的情况下我们可以关闭IE8的增强安全配置,操作很简单如下步骤。 2、进行关闭安全设置 以本机管理员或是域管理员的身份登陆系统,在“开始”菜单-->“管理工具”-->“服务器管理器”,如下图:(或者点击任务栏上的服务器管理器图标即可) 或者在“开始”菜单-->“运行”中输入“servermanager.msc”回车即可,如下图: 在打开的服务器管理器窗口中选中“服务器管理器”,然后单右边窗口中的“配置 IE ESC”如下图: 3、关闭IE,然后进行重新访问百度
1、通过VS2010打开项目链接VSS后,提示 Access to file"\\***\rights.dat" denied。 该提示是指没有网络访问的权限,用户要在共享文件夹有可写的权限才可以。我们在设置共享文件的时候应该允许写入。 2、在Windows2003中设置共享的写入权限有两个地方需要设置everyone的写入权限。 共享: 设置Everyone的更改权限 安全:加入Everyone的写入权限 如果还有问题,就把everyone的权限全部勾上。
前言 不得不说最近三个月都没更新博客了,除了6月初的一篇博客外,今天的这一篇算是这三个月里发表的第二篇博客了。不过本人几乎每天都在博客园里刷来刷去,看大家发表的博文,从中汲取营养。确实博客园也可以算是我的良师益友了,自从恋上了博客,我甚至把之前喜欢玩的网游都戒了,我很庆幸也很高兴,找到了新的玩伴,那就是编程。在不知不觉中,向左侧一看,我呆在博客园也有2年4个月的时间了,说不上长但也不算短,恰恰是在这段时间,博客园教会了我一些做人做事的道理,慢慢的才让我有了之前几个月的想法,那就是尝试建一个属于自己的博客网站。 为什么要建立属于自己的博客网站aehyok.com http://www.cnblogs.com/xishuai/p/3900217.html 不知道你是否有兴趣来看看这篇文章,这是博客园@田园里的蟋蟀最近发表的文章,只是没发不出来而已.我虽然没有真正的和他打过招呼,但仿佛我们就是知己一般。 http://news.cnblogs.com/n/501351/,http://news.cnblogs.com/n/211332/其实这样的博文太多了,并促使我不断的学着去尝试,然后现在我尝试着自己来做了,就有了现在的aehyok.com,当然你这样做,并不会给你带来任何的盈利,反而要进行小额的投资。 http://www.cnblogs.com/donghongtao/p/3611623.html再来看看这篇博文,不知道你们有何感想,我是感概万千啊,认为是软文的就pass掉吧。 看到该博主的成就,我很兴奋。这也可以看作我目前努力的一个方向,目标远大,只希望自己能一步一步的离目标越来越近(当然几乎永远也达不到)。虽然自己现在的工资还是四位数而且……,但这又能怎样,从现在开始坚持一步一个脚印的往前走吧,即使走的慢,但也总有到达终点的那一天。那就加油吧。 aehyok.com的历程 1、注册并购买万网域名,域名没什么意义,只是自己一直使用这个昵称而已。看着域名不是特别贵,所以一下子就搞了个10年的。 2、注册并购买阿里云服务器。服务器的选择真是颇为周折,始终没有找到便宜而有适合自己的,最后只好选择了博客园首页经常看到的广告中的产品,也是博客园使用的产品吧。 3、测试服务器并选择配置。因为自己要做的是.NET程序,所以Windows操作系统是不二的选择。为了节省开支选择的最低配的服务器,这其中支持Windows的最低内存是1G。 因为服务器可以当时可以退订,所以自己又花钱配置了一个Windows Server2012的进行测试,不过最终还是选择的WS2008 R2。 4、备案还是比较轻松,根据步骤一步一步来就可以了,周期也不是很长,也比较顺利。从首次提交到最终审核通过,也就半个月的时间。 aehyok.com网站初步规划 1、对接博客园,将博客园中自己的博客通过代码显示在自己的网站中。 2、对接博客园自己的英语小贴士,我最近写的博文中的最下面都加入了一个小模块 而现在这一切已经汇总起来了在我的一个页面中完全显示了这个列表 3、对接自己的博客系统,前台页面基本已处理OK,后台还需要进行处理。 4、网站更新历史页,记录网站成长点滴 当然还会有5,6,7,8……100,101,102……需要做的东西太多了,而且自己时间又很有限,所以就要合理规划自己的业余时间。就要多进行思考,对过去进行总结和对未来一个周一个月进行计划,虽然计划不如变化快,但如果你没有计划,而变化来了,你又如何来应对变化呢?只有你有了计划,而变化来了,你才可以在原有计划中进行合理的调整变化。 暂时时间有限,所以主要以实现功能为主,页面UI的视觉效果就没考虑那么多,我就是看到比较好的,我就是把感觉别人不错的效果引用过来了,能看就差不多了,请各位看官不要在意。 总结 还记得自己去年年底的总结和规划http://www.cnblogs.com/aehyok/p/3495685.html,依稀的感觉时间是那么的来去匆匆,一年又将过去,而自己虽然按照自己的计划在进行,但脚步的确慢了不少,或者说自己浪费了不少时间,不在过多的给自己找借口了,从现在开始要稍微加快自己的步伐了,也就是要多利用一些可以利用的时间,不能再虚度光阴了。自己的学习重心暂时主要以经营自己的小博客网站为主,等到小博客网站初步规划完成,那么接下来会继续更新自己之前没有完成的系列博文,最起码做到有始有终嘛。 以下是我的各种联系方式,暂时主要以QQ群为主,如果你有Skype账号也可以加我:-O。 首先声明一下,本人技术很菜,本着一种学习的目的,如果你想和我,和大家一起学习MVC,学习EF,学习BootStrap,欢迎加入Q群:206058845,加群验证码为:aehyok。现在Q群暂时只有一人,哈哈,刚建不久的Q群,欢迎各位看官的加入,期待我们的共同成长。
2023年04月