2021 即将结束,回顾这魔幻的一年。
不知道是什么原因导致的,也不知道是从哪一天开始的,莫名峡部裂这种疾病就发生在我身上,这种疾病在骨科领域比较常见,有天生也有后天造成的,在2020年中检查身体发现后,终于在2020年的最后一天决定去做了一场峡部裂的手术,手术很顺利,也很痛苦。术后的恢复流程也记录在知乎上了,希望对相关的病人有帮助。
手术记录
当时由于手术影响,重新思考过一段人生,重新审视自己这一生应该如何度过,对未来非常的迷茫,恰逢公务员考试马上要举行了,于是就报名参加公务员考试了,2月中下旬向领导提出离职。
但是后来到快考试的时候,发现一个七七四十九线的小城市,一个只招聘 1 人的公务员职位竟有 100 多人报名,我又心生退意,除此之外还有以下原因:
于是乎,在 3 月中下旬又重新找工作,原本想去深圳看看的,因为女朋友在深圳工作,但是广州的生活节奏明显更慢,受不了深圳那边的风气,面过一家公司,leader 直接说公司实行 996 ,后续还是没有去深圳,因为在广州找到了一家福利待遇还不错的公司。
这一年也是进步很多的一年,学习到了很多东西。
3 月份写了用 hexo 重新写了一个博客主题: hexo-theme-omega。
5 月份重新学习了一下 rollup 打包和脚手架相关,然后封装一个可以快速开发 react 组件的脚手架及模板。
6 - 8 月开始接触 k8s ,以前只用过 docker ,这段时间开始学会了 k8s 的基本用法。开始接触 dart、flutter、golang,用 golang 刷了一点 LeetCode,用 gin 框架写了一点东西,也用 flutter 写了一点 app demo。
9 月尝试自己写一个 nodejs 后端框架 halucy ,原本计划这个框架是分三部分:
10 月学习了一下 webpack 5 的模块联邦、深入学习了一下 nodejs、tailwind CSS。
11 月发现我的博客主题使用的翻译插件不能用了,于是写了一个 hexo-pinyin-plugin。在公司利用以前的知识搭建了一套前端开发环境下的 Jenkins 部署脚本,学习了一些 Jenkins 的 pipeline 的语法。
12 月重新学起了三年前自己喜欢的一个 nodejs 框架 - Adonisjs,Adonisjs 在 v5 后,对 typescript 支持更友好了,功能和使用上较之前改动挺大的,现在尝试用 Adonisjs 写一个中文文档社区,基于 tailwind CSS 和 zepto.js 写一个前后端不分离的响应式网站。在这个前后端大分离的时代或许是一种倒退的行为?哈哈~~~
我发现目标这种东西不能定的太固定,因为很难真正完成,对自己打击比较大,只能定一个大的方向。想起自己去年定的目标和实际情况真的大相径庭(去年原本是想看 react 源码、写一个像 ant design 一样的 UI 库、300 道 LeetCode…)。所以 2022 的目标,只定大方向。
一时之间竟不知明年的目标定什么好。不过这也没事,路要一步一步走。
看源码、刷题这种事也许会做。。。
今年除了上述列的以外,还有其他一些工作上的就不列上去了,在工作中也有收获,接触和学习了好几种前端微服务的架构,学习了像 mobx 等状态管理库,nextjs 服务端渲染等。
2022 加油!!!
(完)
]]>使用 Jenkins 编写 pipeline 脚本做 CI/CD。顺便分享一些我的 CI/CD 理念。
在没有CI/CD之前,只能手动打包、上传文件、部署到服务器,不仅部署繁琐,而且会反复产生无意义的信息和浪费资源,而且还容易造成信息差,测试人员也不确定我们部署有没有成功,我们也不确定打包后是否正常。
举个例子:
提交代码后很自信的把代码合并进去并且把源分支删除,部署上服务器后,测试人员告诉我:“这里不合格,你在优化一下“。我只能再重新从 develop 分支上 checkout 一个新分支 fix-xxx 再做一些优化,部署后,测试人员发现另外一个地方又出现问题了,打回来要重新再修改一下,如此反复,只能一遍一遍的更改,造成时间上的浪费。
下面分享一种我的 CI/CD 做法。
先看一张图,我们假定我们是按照这个 git 流程规范开发的:
关键点:
这是一个比较理想状态下的 git 流程,但是问题在于,作为开发者,你的代码必须合并到 develop 分支后才能部署,然后让测试人员测试,这样就会带来上面的问题,造成反反复复的修改。所以就又了下面这个优化后的 git 流程规范:
我们规定当开发者修复或者新增了某个功能,唯一合并到主开发分支的途径是通过线上发起 Merge Request,保证主干分支上的整洁。
其次我们可以充分利用 gitlib 的 label 特性,新增了一个 REVIEWED 和 PASSED_TEST 两个标签,只有当一个 Merge Request 拥有这两个标签的时候,才提醒 maintainer 该分支允许合并,且经过了测试和别人的 review。通过此方法保证主开发分支纯净以及降低主分支的缺陷概率。
那我们应该如何在多个开发者代码各自都没有合并的情况下,部署服务呢?
我们可以利用 Jenkins,创建一个自动化部署的工程,对于我的项目来说,指定了以下4个参数:
Source Branch:源分支(新分支)
Target Branch: 目标分支(需要合并到哪个分支,例如 dev)
Reset Target Branch: 是否需要净化目标分支(相当于重置部署,会丢失其他已部署但未合并的分支功能)
Run npm install:是否需要安装依赖(没有引入新的依赖就不用选,增加部署速度)
在开发者提交代码并且推送分支到 gitlib 的时候,可以依据当前实际情况选择部署方式,然后点
击开始构建按钮,整个流程就结束了,剩下的就交给服务器去构建了。
可以直接对接企业微信,通知到群消息:
下面是我的 pipeline 配置:
主要依赖以下几个插件:
pipeline { agent any parameters { gitParameter branch: '', branchFilter: 'origin/(.*)', defaultValue: 'develop', description: '源分支,需要合并到目标分支并部署的分支', name: 'SOURCE_BRANCH', quickFilterEnabled: false, selectedValue: 'NONE', sortMode: 'NONE', tagFilter: '*', type: 'PT_BRANCH' gitParameter branch: '', branchFilter: 'origin/(.*)', defaultValue: 'develop', description: '目标分支', name: 'TARGET_BRANCH', quickFilterEnabled: false, selectedValue: 'NONE', sortMode: 'NONE', tagFilter: '*', type: 'PT_BRANCH' booleanParam defaultValue: false, description: '是否需要净化目标分支', name: 'RESET_TARGET_BRANCH' booleanParam defaultValue: false, description: '是否需要安装依赖(没有引入新的依赖一般不选)', name: 'RUN_NPM_INSTALL' } environment { STAGING_BRANCH = 'develop-staging' // 线上的部署分支 GIT_CRED = credentials("4640b4d1-f3c2-47a6-8668-d043704444a8") // 这个是配置在全局的 git 凭据 } stages{ stage('执行开始构建企微推送通知') { steps { wrap([$class: 'BuildUser']) { script { BUILD_USER = "${env.BUILD_USER}" } } script { def start = new Date().format('yyyy-MM-dd HH:mm:ss') def head = "\"构建${JOB_NAME}:#${env.BUILD_ID}<font color=\\\"comment\\\">开始</font>,详细信息如下:" def s1 = ">源分支:<font color=\\\"comment\\\">${SOURCE_BRANCH}</font>" def s2 = ">目标分支:<font color=\\\"comment\\\">${TARGET_BRANCH}</font>" def s3 = ">是否需要净化目标分支:<font color=\\\"comment\\\">${RESET_TARGET_BRANCH}</font>" def s4 = ">是否需要安装依赖:<font color=\\\"comment\\\">${RUN_NPM_INSTALL}</font>" def s5 = ">部署时间:<font color=\\\"comment\\\">${start}</font>" def s6 = ">部署人:<font color=\\\"comment\\\">${BUILD_USER}</font>" def msg = "${head}" + "\n" + "${s1}" + "\n" + "${s2}" + "\n" + "${s3}" + "\n" + "${s4}" + "\n" + "${s5}" + "\n" + "${s6}\" " echo "${msg}" def body = "{ \"msgtype\": \"markdown\", \"markdown\": { \"content\": ${msg} } }" echo "${body}" httpRequest contentType: 'APPLICATION_JSON_UTF8', httpMode: 'POST', requestBody: "${body}", responseHandle: 'NONE', url: 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxxxxxxxxxxxxxxxxxxxx', // 企微群webhook地址 wrapAsMultipart: false } } } stage("执行初始化git") { steps { checkout([$class: 'GitSCM', branches: [[name: '${SOURCE_BRANCH}'], [name: '${TARGET_BRANCH}'], [name: '${STAGING_BRANCH}']], extensions: [], userRemoteConfigs: [[credentialsId: '4640b4d1-f3c2-47a6-8668-d043704444a8', url: 'https://xxxxxxxxxxxxxxx.git']]]) // 需要替换 git 地址 } } stage("执行处理分支关系"){ steps{ script { if (RESET_TARGET_BRANCH == 'true') { sh 'sudo git checkout ${TARGET_BRANCH}' sh 'sudo git checkout -B ${STAGING_BRANCH}' sh 'sudo git merge origin/${SOURCE_BRANCH}' } else { sh 'sudo git checkout ${STAGING_BRANCH}' sh 'sudo git merge origin/${SOURCE_BRANCH}' } } } } stage("执行安装依赖和打包") { steps { nodejs('nodejs') { script { if (RUN_NPM_INSTALL == 'true') { sh 'echo "开始安装依赖"' sh 'npm install' } else { sh 'echo "不需要安装依赖"' } sh 'npm run build' } } } } stage("执行复制打包后文件到指定目录") { steps { sh 'scp -r dist/* xxxx@192.168.7.136:/xxxxxx' // 复制文件到指定服务器指定目录 } } stage('执行构建成功企微推送通知') { steps { wrap([$class: 'BuildUser']) { script { BUILD_USER = "${env.BUILD_USER}" } } script { def start = new Date().format('yyyy-MM-dd HH:mm:ss') def head = "\"构建${JOB_NAME}:#${env.BUILD_ID}<font color=\\\"info\\\">成功</font>,详细信息如下:" def s1 = ">源分支:<font color=\\\"comment\\\">${SOURCE_BRANCH}</font>" def s2 = ">目标分支:<font color=\\\"comment\\\">${TARGET_BRANCH}</font>" def s3 = ">是否需要净化目标分支:<font color=\\\"comment\\\">${RESET_TARGET_BRANCH}</font>" def s4 = ">是否需要安装依赖:<font color=\\\"comment\\\">${RUN_NPM_INSTALL}</font>" def s5 = ">部署时间:<font color=\\\"comment\\\">${start}</font>" def s6 = ">部署人:<font color=\\\"comment\\\">${BUILD_USER}</font>" def msg = "${head}" + "\n" + "${s1}" + "\n" + "${s2}" + "\n" + "${s3}" + "\n" + "${s4}" + "\n" + "${s5}" + "\n" + "${s6}\" " echo "${msg}" def body = "{ \"msgtype\": \"markdown\", \"markdown\": { \"content\": ${msg} } }" echo "${body}" httpRequest contentType: 'APPLICATION_JSON_UTF8', httpMode: 'POST', requestBody: "${body}", responseHandle: 'NONE', url: 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxxxxxxxxxxxxxxxxxxxx', // 企微群webhook地址 wrapAsMultipart: false } } } } post { failure { echo '部署失败' wrap([$class: 'BuildUser']) { script { BUILD_USER = "${env.BUILD_USER}" } } script { def start = new Date().format('yyyy-MM-dd HH:mm:ss') def head = "\"构建${JOB_NAME}:#${env.BUILD_ID}<font color=\\\"warning\\\">失败</font>,详细信息如下:" def s1 = ">源分支:<font color=\\\"comment\\\">${SOURCE_BRANCH}</font>" def s2 = ">目标分支:<font color=\\\"comment\\\">${TARGET_BRANCH}</font>" def s3 = ">是否需要净化目标分支:<font color=\\\"comment\\\">${RESET_TARGET_BRANCH}</font>" def s4 = ">是否需要安装依赖:<font color=\\\"comment\\\">${RUN_NPM_INSTALL}</font>" def s5 = ">部署时间:<font color=\\\"comment\\\">${start}</font>" def s6 = ">部署人:<font color=\\\"comment\\\">${BUILD_USER}</font>" def msg = "${head}" + "\n" + "${s1}" + "\n" + "${s2}" + "\n" + "${s3}" + "\n" + "${s4}" + "\n" + "${s5}" + "\n" + "${s6}\" " echo "${msg}" def body = "{ \"msgtype\": \"markdown\", \"markdown\": { \"content\": ${msg} } }" echo "${body}" httpRequest contentType: 'APPLICATION_JSON_UTF8', httpMode: 'POST', requestBody: "${body}", responseHandle: 'NONE', url: 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxxxxxxxxxxxxxxxxxxxx', // 企微群webhook地址 wrapAsMultipart: false } } }}
Jenkins 有很多种部署方案,之前一直用的是界面化的配置,这几天刚好有时间学习了一些 pipeline 的方式,这也是我认为比较完善的一种方法。
]]>模块联邦( Module Federation ) 是 webpack5 的一个新特性,它可以使程序像搭积木一样,分成多个模块共同开发,可以作为一种前端微服务的解决方案。
模块联邦是 webpack 的一个插件,使用非常简单,只需要在 webpack 的 plugins 下添加即可:
const { ModuleFederationPlugin } = require("webpack").container;...plugins: [ new ModuleFederationPlugin({ name: "module_federation_lib", // 名字必须唯一,不能用中划线 '-' 分隔 filename: "remoteEntry.js", // 入口文件 exposes: { // 暴露出去的路径及依赖 "./react": "react", "./react-dom": "react-dom", }, }), ],...
例如上述代码,定义一个module_federation_lib
库,向外暴露 react 和 react-dom 两个库,入口文件是 remoteEntry.js ,使用的时候同样在 webpack 下的 plugins 添加如下代码:
new ModuleFederationPlugin({ name: "subapp1", // 名字必须唯一,不能用中划线 '-' 分隔 filename: "remoteEntry.js",// 入口文件 exposes: { // 暴露出去的路径及依赖 "./index": "./app.js", }, remotes: { // 引用的模块 "module-federation-lib": "module_federation_lib@http://localhost:6780/remoteEntry.js", // 指定远程 module_federation_lib 路径 }, }),
就可以在代码中使用:
import React, { Component } from "module-federation-lib/react";
注意:一个常规的 import 是同步的,并不会等待模块下载。这里将所有的异步 remote module 的加载处理成了异步语义(就像 import() )(注:但是需要一个异步加载的边界,)。 此时,就可以在加载本 chunk 的其他普通模块的同时,通过容器并行加载 remote module 。
注意:异步加载边界不好理解,可以用 webpack 文档中 Module Federation 介绍中的一句话来说明:加载 remote module ,必须在任意一个异步 chunk 加载之后执行。所以官方 webpack repo 中给出的例子,直接在入口处添加一个 import(‘boostrap’) 即可,也可以查看项目下的代码示例。
本项目分为四个项目:
分别进入项目安装依赖,然后分别启动即可。
使用模块联邦后的 demo 如下图所示:
具体配置可查看相关代码.
模块联邦提供了一种全新的机制,一方面可以隔离应用,另一方面可以复用应用之间的公共依赖,适合大项目的微服务化,而且还有提升项目打包速度,优化项目体积等作用。
根据其官方文档,甚至可以通过修改 url 传参来响应不同的子应用版本,如下:
plugins: [ new ModuleFederationPlugin({ name: 'host', remotes: { app1: `promise new Promise(resolve => { const urlParams = new URLSearchParams(window.location.search) const version = urlParams.get('app1VersionParam') // This part depends on how you plan on hosting and versioning your federated modules const remoteUrlWithVersion = 'http://localhost:3001/' + version + '/remoteEntry.js' const script = document.createElement('script') script.src = remoteUrlWithVersion script.onload = () => { // the injected script has loaded and is available on window // we can now resolve this Promise const proxy = { get: (request) => window.app1.get(request), init: (arg) => { try { return window.app1.init(arg) } catch(e) { console.log('remote container already initialized') } } } resolve(proxy) } // inject this script with the src set to the versioned remoteEntry.js document.head.appendChild(script); }) `, }, // ... }), ],
这可能是目前最好的微服务方案了。
Demo 源码地址: https://github.com/kavience/module-federation-demo
参考文档:
(完)
]]>使用 nodejs 自带命令参数 prof 以及 chrome dev tool 和 ab 工具进行 nodejs 项目性能分析。
好久没写博客了,今天看到一个关于 nodejs 性能分析的视频,特此记录一下。
刚好工作中有用到 nodejs 写项目,也在尝试自己写一个基于 nodejs 的 restful 框架。
首先 node 是自带性能分析的,使用以下命令
node --prof xxx.js
增加 prof
参数会在与应用程序的本地运行相同的目录中生成了一个刻度文件。形式为 isolate-0xnnnnnnnnnnnn-v8.log (其中 n 为数字)。
如果是一个 web 项目,可以使用 ab 工具进行压力测试,以增加更多分析数据。例如:
ab -c50 -t10 http://127.0.0.1:8080/xxxx
nodejs 会自动把进程信息存储在 isolate-0xnnnnnnnnnnnn-v8.log 内,该文件根本无法阅读,需要使用 nodejs 的刻度处理器,使用 --prof-process
参数。
node --prof-process isolate-0xnnnnnnnnnnnn-v8.log > analysis.txt
查看 analysis.txt 就可以看到更适合阅读的格式,
查看关于 prof
的更多详解
自带的分析工具,还是不够直观,而且操作不够方便,nodejs 本身是基于 v8 引擎开发的,所以我们可以直接使用 chrome 的开发工具进行调试项目。
node --inspect-brk xxx.js
使用 inspect-brk
参数可以启动调试,启动后会开启一个 wobsocket 服务。
我们可以在 chrome 浏览器地址栏中输入 chrome://inspect
进入开发工具,可以发现 target 下有一个 inspect
按钮,点击就可以进入调试模式了。接下来就和前端调试一样了,可以在 dev-tool 中看到源码、记录快照以及打印结果。
查看关于 inspect-brk
的更多详解
本文只简单记录,不够完善,以后有空再来深入了解。
(完)
]]>记录工作中一些常用的命令。
使用的有点频繁,方便以后查找。
没有 url 的话默认登录到 https://hub.docker.com/,可以使用 habor 自建容器仓库
docker login $url
在Dockerfile所在目录下,确保Dockerfile中语法无误的情况下;运行
docker build -t $image_name:$tag_name .
完成之后通过docker images
或者docker image ls
检查是否创建成功
docker run -it $image_name:$tag_name /bin/bash
完成之后通过 docker ps -a
检查镜像是否创建成功
docker rm $container_ID
docker rmi $image_name:$tag_name
docker exec -it $container_ID /bin/bash
如果容器未启动,则需用 docker start $container_ID
先启动该容器
导出镜像文件
docker save $container_ID > $name
上传
docker push [OPTIONS] NAME[:TAG]
拉取
docker pull [OPTIONS] NAME[:TAG|@DIGEST]
前提是需在宿主机中执行
sudo docker cp $host_path $container_ID:container_path
感觉容器化是一个趋势,尤其是针对项目将来需要 SaaS 独立部署,目前只记录一些简单命令,不够完善。
更多命令:https://docs.docker.com/engine/reference/commandline/pull/
(完)
]]>记录工作中一些常用的命令。
使用的有点频繁,方便以后查找。
创建 pod
sudo kubectl create -f <yaml文件> -n <命名空间>
删除 pod
sudo kubectl delete -f <yaml文件> -n <命名空间>
强制删除 pod
sudo kubectl delete pod <pod名字> -n eip-release --grace-period=0 --force
查看命名空间
sudo kubectl get namespace
查看命名空间下的 pods 且分组
sudo kubectl get pods -n <命名空间> |grep <关键字>
查看pod描述
sudo kubectl describe pod <pod 名字> -n <命名空间>
查看日志
sudo kubectl logs <pod名字> -n <命名空间> -c <container>
查看 ingress
sudo kubectl get ingress -A
进入pod
kubectl exec -ti <pod名字> -n <命名空间> -- sh
删除 pvc
sudo kubectl patch pvc <pvc名字> -p '{"metadata":{"finalizers":null}}'
删除 deployment
sudo kubectl delete deployment <deployment名字> -n <命名空间>
编辑configmap
sudo kubectl edit configmap sfa-wxwork-api-cm -n <命名空间>
感觉容器化是一个趋势,尤其是针对项目将来需要 SaaS 独立部署,目前只记录一些简单命令,不够完善。
(完)
]]>雷军-创业第一课 总结
学习雷军精神
听完雷军的演讲,感觉对雷军有了新的认识,对创业也有了完全不一样的认识。
(完)
]]>研究了一下 golang 相关的文档,写下这篇 golang 入门日记,包含 golang 基础教程,这是 golang 语言初探第三篇。
周末的时候研究了一下 golang 相关的文档,根据自己的工作经验,我觉得这门语言在未来一定会大放异彩,其实现在也比较热门了。主要有以下几个特点:
经过对 golang 的这些了解,顿时感觉有点兴趣了,在这里记录一些入门的资料。
if 语句一般会由关键字 if 、条件表达式和由花括号包裹的代码块组成。所谓代码块,即是包含了若干表达式和语句的序列。在 Go 语言中,代码块必须由花括号包裹。另外,这里的条件表达式是指其结果类型是 bool 的表达式。一条最简单的 if 语句可以是:
if 100 > number { number += 3}
这里的标识符 number 可以代表一个 int 类型的值。这条 if 语句的意思是:如果 number 的值小于 100 ,那么就把其值增加 3 。我还可以在此之上添加 else 分支,就像这样:
if 100 > number { number += 3} else { number -= 2}
else 分支的含义是,提供在条件不成立(具体到这里是 number 的值不小于 100 )的情况下需要执行的操作。除此之外, if 语句还支持串联。请看下面的例子:
if 100 > number { number += 3} else if 100 < number { number -= 2} else { fmt.Println("OK!")}
可以看到,上述代码很像是把多条 if 语句串接在一起了一样。这样的 if 语句用于对多个条件的综合判断。上述语句的意思是,若 number 的值小于 100 则将其加 3 ,若 number 的值大于 100 则将其减 2 ,若 number 的值等于 100 则打印 OK! 。
注意,我们至此还未对 number 变量进行声明。上面的示例也因此不能通过编译。
我们可以用单独的语句来声明该变量并为它赋值。但是我们也可以把这样的变量赋值直接加入到 if 子句中。示例如下:
if number := 4; 100 > number { number += 3} else if 100 < number { number -= 2} else { fmt.Println("OK!")}
这里的 number := 4
被叫做 if 语句的初始化子句。它应被放置在 if 关键字和条件表达式之间,并与前者由空格分隔、与后者由英文分号;分隔。注意,我们在这里使用了短变量声明语句,即:在声明变量 number 的同时为它赋值。这意味着这里的 number 被视为一个新的变量。它的作用域仅在这条 i 语句所代表的代码块中。也可以说,变量 number 对于该 if 语句之外的代码来说是不可见的。我们若要在该 if 语句以外使用 number 变量就会造成编译错误。
另外还要注意,即使我们已经在这条if语句所代表的代码块之外声明了 number 变量,这里的语句 number := 4 也是合法的。请看这个例子:
var number intif number := 4; 100 > number { number += 3} else if 100 < number { number -= 2} else { fmt.Println("OK!")}
这种写法有一个专有名词,叫做:标识符的重声明。实际上,只要对同一个标识符的两次声明各自所在的代码块之间存在包含的关系,就会形成对该标识符的重声明。具体到这里,第一次声明的 number 变量所在的是该 if 语句的外层代码块,而 number := 4 所声明的 number 变量所在的是该if语句的代表代码块。它们之间存在包含关系。因此对 number 的重声明就形成了。
这种情况造成的结果就是, if 语句内部对 number 的访问和赋值都只会涉及到第二次声明的那个 number 变量。这种现象也被叫做标识符的遮蔽。上述代码被执行完毕之后,第二次声明的 number 变量的值会是7,而第一次声明的 number 变量的值仍会是 0 。
与串联的 if 语句类似, switch 语句提供了一个多分支条件执行的方法。不过在这里用一个专有名词来代表分支——case
。每一个 case 可以携带一个表达式或一个类型说明符。前者又可被简称为 case 表达式。因此, Go 语言的 switch 语句又分为表达式 switch 语句和类型 switch 语句。
先说表达式 switch 语句。在此类 switch 语句中,每个 case 会携带一个表达式。与 if 语句中的条件表达式不同,这里的 case 表达式的结果类型并不一定是 bool 。不过,它们的结果类型需要与 switch 表达式的结果类型一致。所谓 switch 表达式是指 switch 语句中要被判定的那个表达式。 switch 语句会依据该表达式的结果与各个 case 表达式的结果是否相同来决定执行哪个分支。请看下面的示例:
var name string// 省略若干条语句switch name {case "Golang": fmt.Println("A programming language from Google.")case "Rust": fmt.Println("A programming language from Mozilla.")default: fmt.Println("Unknown!")}
可以看到,在上述 switch 语句中, name 充当了 switch 表达式,而" Go “和” Rust "充当了 case 表达式。它们的结果类型是一致的,都是 string 。顺便说一句,可以有只包含一个字面量或标识符的表达式。它们是最简单的表达式,属于基本表达式的一种。
请大家注意 switch 语句的写法。 switch 表达式必须紧随 switch 关键字出现。在后面的花括号中,一个关键字 case、case 表达式、冒号以及后跟的若干条语句组成为一条 case 语句。在 switch 语句中可以有若干条 case 语句。 Go 语言会依照从上至下的顺序对每一条 case 语句中 case 表达式进行求值。只要被发现其表达式与 switch 表达式的结果相同,该 case 语句就会被选中。它包含的那些语句就会被执行。而其余的 case 语句则会被忽略。
switch 语句中还可以存在一个特殊的 case——default case 。顾名思义,当没有一个常规的 case 被选中的时候,default case 就会被选中。上面示例中就存在一个default case 。它由关键字 default 、冒号和后跟的一条语句组成。实际上, default case 不一定被追加在最后。它可以是第一个 case ,或者出现在任意顺位上。
另外,与 if 语句一样, switch 语句还可以包含初始化子句,且其出现位置和写法也如出一辙。如:
names := []string{"Golang", "Java", "Rust", "C"}switch name := names[0]; name {case "Golang": fmt.Println("A programming language from Google.")case "Rust": fmt.Println("A programming language from Mozilla.")default: fmt.Println("Unknown!")}
另一个类型 switch 语句。它与一般形式有两点差别。第一点,紧随 case 关键字的不是表达式,而是类型说明符。类型说明符由若干个类型字面量组成,且多个类型字面量之间由英文逗号分隔。第二点,它的 switch 表达式是非常特殊的。这种特殊的表达式也起到了类型断言的作用,但其表现形式很特殊,如:v.(type),其中 v 必须代表一个接口类型的值。注意,该类表达式只能出现在类型 switch 语句中,且只能充当 switch 表达式。一个类型 switch 语句的示例如下:
v := 11switch i := interface{}(v).(type) {case int, int8, int16, int32, int64: fmt.Printf("A signed integer: %d. The type is %T. \n", i, i)case uint, uint8, uint16, uint32, uint64: fmt.Printf("A unsigned integer: %d. The type is %T. \n", i, i)default: fmt.Println("Unknown!")}
请注意,我们在这里把 switch 表达式的结果赋给了一个变量。如此一来,我们就可以在该 switch 语句中使用这个结果了。这段代码被执行后,标准输出上会打印出 A signed integer: 11. The type is int.
。
最后,我们来说一下 fallthrough 。它既是一个关键字,又可以代表一条语句。 fallthrough 语句可被包含在表达式 switch 语句中的 case 语句中。它的作用是使控制权流转到下一个 case 。不过要注意, fallthrough 语句仅能作为 case 语句中的最后一条语句出现。并且,包含它的 case 语句不能是其所属 switch 语句的最后一条 case 语句。
for 语句代表着循环。一条语句通常由关键字 for 、初始化子句、条件表达式、后置子句和以花括号包裹的代码块组成。其中,初始化子句、条件表达式和后置子句之间需用分号分隔。示例如下:
for i := 0; i < 10; i++ { fmt.Print(i, " ")}
我们可以省略掉初始化子句、条件表达式、后置子句中的任何一个或多个,不过起到分隔作用的分号一般需要被保留下来,除非在仅有条件表达式或三者全被省略时分号才可以被一同省略。
我们可以把上述的初始化子句、条件表达式、后置子句合称为for子句。实际上,for语句还有另外一种编写方式,那就是用 range 子句替换掉 for 子句。 range 子句包含一个或两个迭代变量(用于与迭代出的值绑定)、特殊标记 := 或 = 、关键字 range 以及 range 表达式。其中, range 表达式的结果值的类型应该是能够被迭代的,包括:字符串类型、数组类型、数组的指针类型、切片类型、字典类型和通道类型。例如:
for i, v := range "Go语言" { fmt.Printf("%d: %c\n", i, v)}
对于字符串类型的被迭代值来说,for语句每次会迭代出两个值。第一个值代表第二个值在字符串中的索引,而第二个值则代表该字符串中的某一个字符。迭代是以索引递增的顺序进行的。例如,上面的 for 语句被执行后会在标准输出上打印出:
0: G1: o2: 语5: 言
可以看到,这里迭代出的索引值并不是连续的。下面我们简单剖析一下此表象的本质。我们知道,字符串的底层是以字节数组的形式存储的。而在 Go 语言中,字符串到字节数组的转换是通过对其中的每个字符进行 UTF-8 编码来完成的。字符串"Go语言"中的每一个字符与相应的字节数组之间的对应关系如下:
注意,一个中文字符在经过 UTF-8 编码之后会表现为三个字节。所以,我们用
语[0]、语[1]和、语[2]
分别表示字符’语’经编码后的第一、二、三个字节。对于字符’言’,我们如法炮制。
对照这张表格,我们就能够解释上面那条 for 语句打印出的内容了,即:每次迭代出的第一个值所代表的是第二个字符值经编码后的第一个字节在该字符串经编码后的字节数组中的索引值。
对于数组值、数组的指针值和切片之来说,range 子句每次也会迭代出两个值。其中,第一个值会是第二个值在被迭代值中的索引,而第二个值则是被迭代值中的某一个元素。同样的,迭代是以索引递增的顺序进行的。
对于字典值来说,range 子句每次仍然会迭代出两个值。显然,第一个值是字典中的某一个键,而第二个值则是该键对应的那个值。
注意,对字典值上的迭代, Go 语言是不保证其顺序的。
携带 range 子句的 for 语句还可以应用于一个通道值之上。其作用是不断地从该通道值中接收数据,不过每次只会接收一个值。注意,如果通道值中没有数据,那么 for 语句的执行会处于阻塞状态。无论怎样,这样的循环会一直进行下去。直至该通道值被关闭,for 语句的执行才会结束。
最后,我们来说一下 break 语句和 continue 语句。它们都可以被放置在 for 语句的代码块中。前者被执行时会使其所属的 for 语句的执行立即结束,而后者被执行时会使当次迭代被中止(当次迭代的后续语句会被忽略)而直接进入到下一次迭代。
select 语句属于条件分支流程控制方法,不过它只能用于通道。它可以包含若干条 case 语句,并根据条件选择其中的一个执行。进一步说, select 语句中的 case 关键字只能后跟用于通道的发送操作的表达式以及接收操作的表达式或语句。示例如下:
ch1 := make(chan int, 1)ch2 := make(chan int, 1)// 省略若干条语句select {case e1 := <-ch1: fmt.Printf("1th case is selected. e1=%v.\n", e1)case e2 := <-ch2: fmt.Printf("2th case is selected. e2=%v.\n", e2)default: fmt.Println("No data!")}
如果该 select 语句被执行时通道 ch1 和 ch2 中都没有任何数据,那么肯定只有 default case 会被执行。但是,只要有一个通道在当时有数据就不会轮到 default case 执行了。显然,对于包含通道接收操作的 case 来讲,其执行条件就是通道中存在数据(或者说通道未空)。如果在当时有数据的通道多于一个,那么 Go 语言会通过一种伪随机的算法来决定哪一个 case 将被执行。
另一方面,对于包含通道发送操作的 case 来讲,其执行条件就是通道中至少还能缓冲一个数据(或者说通道未满)。类似的,当有多个 case 中的通道未满时,它们会被随机选择。请看下面的示例:
ch3 := make(chan int, 100)// 省略若干条语句select {case ch3 <- 1: fmt.Printf("Sent %d\n", 1)case ch3 <- 2: fmt.Printf("Sent %d\n", 2)default: fmt.Println("Full channel!")}
该条 select 语句的两个 case 中包含的都是针对通道 ch3 的发送操作。如果我们把这条语句置于一个循环中,那么就相当于用有限范围的随机整数集合去填满一个通道。
请注意,如果一条 select 语句中不存在 default case , 并且在被执行时其中的所有 case 都不满足执行条件,那么它的执行将会被阻塞!当前流程的进行也会因此而停滞。直到其中一个 case 满足了执行条件,执行才会继续。我们一直在说 case 执行条件的满足与否取决于其操作的通道在当时的状态。这里特别强调一点,即:未被初始化的通道会使操作它的 case 永远满足不了执行条件。对于针对它的发送操作和接收操作来说都是如此。
最后提一句, break 语句也可以被包含在 select 语句中的 case 语句中。它的作用是立即结束当前的 select 语句的执行,不论其所属的 case 语句中是否还有未被执行的语句。
与 select 语句一样, Go 语言中的 defer 语句也非常独特,而且比前者有过之而无不及。 defer 语句仅能被放置在函数或方法中。它由关键字 defer 和一个调用表达式组成。注意,这里的调用表达式所表示的既不能是对 Go 语言内建函数的调用也不能是对 Go 语言标准库代码包 unsafe 中的那些函数的调用。实际上,满足上述条件的调用表达式被称为表达式语句。请看下面的示例:
func readFile(path string) ([]byte, error) { file, err := os.Open(path) if err != nil { return nil, err } defer file.Close() return ioutil.ReadAll(file)}
函数 readFile 的功能是读出指定文件或目录(以下统称为文件)本身的内容并将其返回,同时当有错误发生时立即向调用方报告。其中, os 和 ioutil (导入路径是 io/ioutil )代表的都是 Go 语言标准库中的代码包。请注意这个函数中的倒数第二条语句。我们在打开指定文件且未发现有错误发生之后,紧跟了一条 defer 语句。其中携带的表达式语句表示的是对被打开文件的关闭操作。注意,当这条 defer 语句被执行的时候,其中的这条表达式语句并不会被立即执行。它的确切的执行时机是在其所属的函数(这里是 readFile )的执行即将结束的那个时刻。也就是说,在 readFile 函数真正结束执行的前一刻, file.Close() 才会被执行。这也是 defer 语句被如此命名的原因。我们在结合上下文之后就可以看出,语句 defer file.Close() 的含义是在打开文件并读取其内容后及时地关闭它。该语句可以保证在 readFile 函数将结果返回给调用方之前,那个文件或目录一定会被关闭。这实际上是一种非常便捷和有效的保险措施。
更为关键的是,无论 readFile 函数正常地返回了结果还是由于在其执行期间有运行时恐慌发生而被剥夺了流程控制权,其中的 file.Close() 都会在该函数即将退出那一刻被执行。这就更进一步地保证了资源的及时释放。
注意,当一个函数中存在多个 defer 语句时,它们携带的表达式语句的执行顺序一定是它们的出现顺序的倒序。下面的示例可以很好的证明这一点:
func deferIt() { defer func() { fmt.Print(1) }() defer func() { fmt.Print(2) }() defer func() { fmt.Print(3) }() fmt.Print(4)}
deferIt 函数的执行会使标准输出上打印出 4321 。请大家猜测下面这个函数被执行时向标准输出打印的内容,并真正执行它以验证自己的猜测。最后论证一下自己的猜测为什么是对或者错的。
func deferIt2() { for i := 1; i < 5; i++ { defer fmt.Print(i) }}
最后,对于 defer 语句,我还有两个特别提示:
func deferIt3() { f := func(i int) int { fmt.Printf("%d ",i) return i * 10 } for i := 1; i < 5; i++ { defer fmt.Printf("%d ", f(i)) }}
它在被执行之后,标准输出上打印出1 2 3 4 40 30 20 10 。
func deferIt4() { for i := 1; i < 5; i++ { defer func() { fmt.Print(i) }() }}
deferIt4 函数在被执行之后标出输出上会出现 5555 ,而不是 4321 。原因是 defer 语句携带的表达式语句中的那个匿名函数包含了对外部(确切地说,是该 defer 语句之外)的变量的使用。注意,等到这个匿名函数要被执行(且会被执行 4 次)的时候,包含该 defer 语句的那条 for 语句已经执行完毕了。此时的变量 i 的值已经变为了 5 。因此该匿名函数中的打印函数只会打印出 5 。正确的用法是:把要使用的外部变量作为参数传入到匿名函数中。修正后的 deferIt4 函数如下:
func deferIt4() { for i := 1; i < 5; i++ { defer func(n int) { fmt.Print(n) }(i) }}
Go 语言的函数可以一次返回多个结果。这就为我们温和地报告错误提供了语言级别的支持。实际上,这也是 Go 语言中处理错误的惯用法之一。
func readFile(path string) ([]byte, error) { file, err := os.Open(path) if err != nil { return nil, err } defer file.Close() return ioutil.ReadAll(file)}
函数 readFile 有两个结果声明。第二个结果声明的类型是 error 。 error 是 Go 语言内置的一个接口类型。它的声明是这样的:
type error interface { Error() string}
显然,只要一个类型的方法集合包含了名为 Error 、无参数声明且仅声明了一个 string 类型的结果的方法,就相当于实现了 error 接口。 os.Open 函数的第二个结果值就的类型就是这样的。我们把它赋给了变量 err 。也许你已经意识到,在 Go 语言中,函数与其调用方之间温和地传递错误的方法即是如此。
在调用了 os.Open 函数并取得其结果之后,我们判断 err 是否为 nil 。如果答案是肯定的,那么就直接把该错误(这里由 err 代表)返回给调用方。这条 if 语句实际上是一条卫述语句。这样的语句会检查流程中的某个步骤是否存在异常,并在必要时中止流程并报告给上层的程序(这里是调用方)。在 Go 语言的标准库以及很多第三方库中,我们经常可以看到这样的代码。我们也建议大家在自己的程序中善用这样的卫述语句。
现在我们把目光聚焦到 readFile 函数中的最后一条语句上。这是一条 return 语句。它把对 ioutil.ReadAll 函数的调用的结果直接作为 readFile 函数的结果返回了。实际上, ioutil.ReadAll 函数的结果声明列表与 readFile 的结果声明列表是一致的。也就是说,它们声明的结果的数量、类型和顺序都是相同的。因此,我们才能够做这种返回结果上的“嫁接”。这又是一个 Go 语言编码中的惯用法。
好了,在知晓怎样在传递错误之后,让我们来看看怎样创造错误。没错,在很多时候,我们需要创造出错误(即 error 类型的值)并把它传递给上层程序。这很简单。只需调用标准库代码包 errors 的 New 函数即可。例如,我们只要在 readFile 函数的开始处加入下面这段代码就可以更快的在参数值无效时告知调用方:
if path == "" { return nil, errors.New("The parameter is invalid!")}
errors.New 是一个很常用的函数。在 Go 语言标准库的代码包中有很多由此函数创建出来的错误值,比如 os.ErrPermission 、 io.EOF 等变量的值。我们可以很方便地用操作符 == 来判断一个 error 类型的值与这些变量的值是否相等,从而来确定错误的具体类别。就拿 io.EOF 来说,它代表了一个信号。该信号用于通知数据读取方已无更多数据可读。我们在得到这样一个错误的时候不应该把它看成一个真正的错误,而应该只去结束相应的读取操作。请看下面的示例:
br := bufio.NewReader(file)var buf bytes.Bufferfor { ba, isPrefix, err := br.ReadLine() if err != nil { if err == io.EOF { break } fmt.Printf("Error: %s\n", err) break } buf.Write(ba) if !isPrefix { buf.WriteByte('\n') }}
可以看到,这段代码使用到了前面示例中的变量 file 。它的功能是把 file 代表的文件中的所有内容都读取到一个缓冲器(由变量 buf 代表)中。请注意,该示例中的第 6 ~ 8 行代码。如果判定 err 代表的错误值等于 io.EOF 的值(即它们是同一个值),那么我们只需退出当前的循环以使读取操作结束即可。
总之,只要能够善用 error 接口、 errors.New 函数和比较操作符==,我们就可以玩儿转 Go 语言中的一般错误处理。
panic 可被意译为运行时恐慌。因为它只有在程序运行的时候才会被“抛出来”。并且,恐慌是会被扩散的。当有运行时恐慌发生时,它会被迅速地向调用栈的上层传递。如果我们不显式地处理它的话,程序的运行瞬间就会被终止。这里有一个专有名词——程序崩溃。内建函数 panic 可以让我们人为地产生一个运行时恐慌。不过,这种致命错误是可以被恢复的。在 Go 语言中,内建函数 recover 就可以做到这一点。
实际上,内建函数 panic 和 recover 是天生的一对。前者用于产生运行时恐慌,而后者用于“恢复”它。不过要注意, recover 函数必须要在 defer 语句中调用才有效。因为一旦有运行时恐慌发生,当前函数以及在调用栈上的所有代码都是失去对流程的控制权。只有 defer 语句携带的函数中的代码才可能在运行时恐慌迅速向调用栈上层蔓延时“拦截到”它。这里有一个可以起到此作用的 defer 语句的示例:
defer func() { if p := recover(); p != nil { fmt.Printf("Fatal error: %s\n", p) }}()
在这条 defer 语句中,我们调用了 recover 函数。该函数会返回一个 interface{} 类型的值。还记得吗? interface{} 代表空接口。 Go 语言中的任何类型都是它的实现类型。我们把这个值赋给了变量 p 。如果 p 不为 nil ,那么就说明当前确有运行时恐慌发生。这时我们需根据情况做相应处理。注意,一旦 defer 语句中的 recover 函数调用被执行了,运行时恐慌就会被恢复,不论我们是否进行了后续处理。所以,我们一定不要只“拦截”不处理。
我们下面来反观 panic 函数。该函数可接受一个 interface{} 类型的值作为其参数。也就是说,我们可以在调用 panic 函数的时候可以传入任何类型的值。不过,我建议大家在这里只传入 error 类型的值。这样它表达的语义才是精确的。更重要的是,当我们调用 recover 函数来“恢复”由于调用 panic 函数而引发的运行时恐慌的时候,得到的值正是调用后者时传给它的那个参数。因此,有这样一个约定是很有必要的。
总之,运行时恐慌代表程序运行过程中的致命错误。我们只应该在必要的时候引发它。人为引发运行时恐慌的方式是调用 panic 函数。 recover 函数是我们常会用到的。因为在通常情况下,我们肯定不想因为运行时恐慌的意外发生而使程序崩溃。最后,在“恢复”运行时恐慌的时候,大家一定要注意处理措施的得当。
go 语句和通道类型是 Go 语言的并发编程理念的最终体现。相比之下, go 语句在用法上要比通道简单很多。与 defer 语句相同, go 语句也可以携带一条表达式语句。
注意,go 语句的执行会很快结束,并不会对当前流程的进行造成阻塞或明显的延迟。一个简单的示例如下:
go fmt.Println("Go!")
可以看到, go 语句仅由一个关键字 go 和一条表达式语句构成。同样的, go 语句的执行与其携带的表达式语句的执行在时间上没有必然联系。这里能够确定的仅仅是后者会在前者完成之后发生。在 go 语句被执行时,其携带的函数(也被称为 go 函数)以及要传给它的若干参数(如果有的话)会被封装成一个实体(即 Goroutine ),并被放入到相应的待运行队列中。 Go 语言的运行时系统会适时的从队列中取出待运行的 Goroutine 并执行相应的函数调用操作。注意,对传递给这里的函数的那些参数的求值会在 go 语句被执行时进行。这一点也是与 defer 语句类似的。
正是由于 go 函数的执行时间的不确定性,所以 Go 语言提供了很多方法来帮助我们协调它们的执行。其中最简单粗暴的方法就是调用 time.Sleep 函数。请看下面的示例:
package mainimport ( "fmt")func main() { go fmt.Println("Go!")}
这样一个命令源码文件被运行时,标准输出上不会有任何内容出现。因为还没等 Go 语言运行时系统调度那个 go 函数执行,主函数 main 就已经执行完毕了。函数 main 的执行完毕意味着整个程序的执行的结束。因此,这个 go 函数根本就没有执行的机会。
但是,当我们在上述 go 语句的后面添加一条对 time.Sleep 函数的调用语句之后情况就会不同了:
package mainimport ( "fmt" "time")func main() { go fmt.Println("Go!") time.Sleep(100 * time.Millisecond)}
语句 time.Sleep(100 * time.Millisecond) 会把 main 函数的执行结束时间向后延迟 100 毫秒。 100 毫秒虽短暂,但足够 go 函数被调度执行的了。上述命令源码文件在被运行时会如我们所愿地在标准输出上打印出 Go! 。
另一个比较绅士的做法是在 main 函数的最后调用 runtime.Gosched
函数。相应的程序版本如下:
package mainimport ( "fmt" "runtime")func main() { go fmt.Println("Go!") runtime.Gosched()}
runtime.Gosched 函数的作用是让当前正在运行的 Goroutine (这里是运行 main 函数的那个 Goroutine )暂时“休息”一下,而让 Go 运行时系统转去运行其它的 Goroutine (这里是与 go fmt.Println("Go!")
对应并会封装 fmt.Println("Go!")
的那个 Goroutine )。如此一来,我们就更加精细地控制了对几个 Goroutine 的运行的调度。
当然,我们还有其它方法可以满足上述需求。并且,如果我们需要去左右更多的 Goroutine 的运行时机的话,下面这种方法也许更合适一些。请看代码:
package mainimport ( "fmt" "sync")func main() { var wg sync.WaitGroup wg.Add(3) go func() { fmt.Println("Go!") wg.Done() }() go func() { fmt.Println("Go!") wg.Done() }() go func() { fmt.Println("Go!") wg.Done() }() wg.Wait()}
sync.WaitGroup 类型有三个方法可用—— Add、 Done 和 Wait 。 Add 会使其所属值的一个内置整数得到相应增加, Done 会使那个整数减 1 ,而 Wait 方法会使当前 Goroutine(这里是运行 main 函数的那个 Goroutine )阻塞直到那个整数为 0 。这下你应该明白上面这个示例所采用的方法了。我们在 main 函数中启用了三个 Goroutine 来封装三个 go 函数。每个匿名函数的最后都调用了 wg.Done 方法,并以此表达当前的 go 函数会立即执行结束的情况。当这三个 go 函数都调用过 wg.Done 函数之后,处于 main 函数最后的那条 wg.Wait() 语句的阻塞作用将会消失, main 函数的执行将立即结束。
本系列只是 golang 入门语法,是根据慕课网-Go语言第一课学习,并记录下来,方便日后查阅,加深印象。感谢慕课网。
(完)
]]>研究了一下 golang 相关的文档,写下这篇 golang 入门日记,包含 golang 基础教程,这是 golang 语言初探第二篇。
周末的时候研究了一下 golang 相关的文档,根据自己的工作经验,我觉得这门语言在未来一定会大放异彩,其实现在也比较热门了。主要有以下几个特点:
经过对 golang 的这些了解,顿时感觉有点兴趣了,在这里记录一些入门的资料。
个数组(Array)就是一个可以容纳若干类型相同的元素的容器。这个容器的大小(即数组的长度)是固定的,且是体现在数组的类型字面量之中的。比如,我们声明了一个数组类型:
type MyNumbers [3]int
注:类型声明语句由关键字type、类型名称和类型字面量组成。
所谓类型字面量,就是用于表示某个类型的字面表示(或称标记方法)。相对的,用于表示某个类型的值的字面表示可被称为值字面量,或简称为字面量。比如之前提到过的3.7E-2就可被称为浮点数字面量。 类型字面量[3]int由两部分组成。第一部分是由方括号包裹的数组长度,即[3]。这也意味着,一个数组的长度是该数组的类型的组成部分,是固定不变的。该类型字面量的第二个组成部分是int。它代表了该数组可以容纳的元素的类型。说到这里,上面这条类型声明语句实际上是为数组类型[3]int声明了一个别名类型。这使得我们可以把 MyNumbers 当做数组类型[3]int来使用。
我们表示这样一个数组类型的值的时候,应该把该类型的类型字面量写在最左边,然后用花括号包裹该值包含的若干元素。各元素之间以(英文半角)逗号分隔,即:
[3]int{1, 2, 3}
现在,我们把这个数组字面量赋给一个名为 numbers 的变量:
var numbers = [3]int{1, 2, 3}
注:这是一条变量声明语句。它在声明变量的同时为该变量赋值。
另一种便捷方法是,在其中的类型字面量中省略代表其长度的数字,像这样:
var numbers = [...]int{1, 2, 3}
这样就可以免去我们为填入那个数字而数出元素个数的工作了。
接下来,我们可以很方便地使用索引表达式来访问该变量的值中的任何一个元素,例如:
numbers[0] // 会得到第一个元素numbers[1] // 会得到第二个元素numbers[2] // 会得到第三个元素
注:索引表达式由字符串、数组、切片或字典类型的值(或者代表此类值的变量或常量)和由方括号包裹的索引值组成。在这里,索引值的有效范围是[0, 3)。也就是说,对于数组来说,索引值既不能小于0也不能大于或等于数组值的长度。另外要注意,索引值的最小有效值总是 0,而不是 1 。
相对的,如果我们想修改数组值中的某一个元素值,那么可以使用赋值语句直接达到目的。例如,我们要修改 numbers 中的第二个元素的话,如此即可:
numbers[1] = 4
虽然数组的长度已经体现在了它的类型字面量,但是我们在很多时候仍然需要明确的获得它,像这样:
var length = len(numbers)
注:len是Go语言的内建函数的名称。该函数用于获取字符串、数组、切片、字典或通道类型的值的长度。我们可以在Go语言源码文件中直接使用它。
最后,要注意,如果我们只声明一个数组类型的变量而不为它赋值,那么该变量的值将会是指定长度的、其中各元素均为元素类型的零值(或称默认值)的数组值。例如,若有这样一个变量:
var numbers2 [5]int
则它的值会是
[5]int{0, 0, 0, 0, 0}
切片(Slice)与数组一样,也是可以容纳若干类型相同的元素的容器。与数组不同的是,无法通过切片类型来确定其值的长度。每个切片值都会将数组作为其底层数据结构。我们也把这样的数组称为切片的底层数组。表示切片类型的字面量如:
[]int// 或[]string
可以看到,它们与数组的类型字面量的唯一不同是不包含代表其长度的信息。因此,不同长度的切片值是有可能属于同一个类型的。相对的,不同长度的数组值必定属于不同类型。对一个切片类型的声明可以这样:
type MySlice []int
这时,类型 MySlice 即为切片类型 []int 的一个别名类型。除此之外,对切片值的表示也与数组值也极其相似,如:
[]int{1, 2, 3}
这样的字面量与数组(值)的字面量的区别也只在于最左侧的类型字面量。
操作数组值的方法也同样适用于切片值。不过,还有一种操作数组值的方法,这种操作的名称就叫“切片”。实施切片操作的方式就是切片表达式。举例如下:
var numbers3 = [5]int{1, 2, 3, 4, 5}var slice1 = numbers3[1:4]
请注意第二条赋值语句中在“ = ”右边那个部分。切片表达式一般由字符串、数组或切片的值以及由方括号包裹且由英文冒号“:”分隔的两个正整数组成。这两个正整数分别表示元素下界索引和元素上界索引。在本例中,切片表达式numbers3[1:4]的求值结果为[]int{2, 3, 4}。可见,切片表达式的求值结果相当于以元素下界索引和元素上界索引作为依据从被操作对象上“切下”而形成的新值。注意,被“切下”的部分不包含元素上界索引指向的元素。另外,切片表达式的求值结果会是切片类型的,且其元素类型与被“切片”的值的元素类型一致。实际上,slice1 这个切片值的底层数组正是 numbers3 的值。
实际上,我们也可以在一个切片值上实施切片操作。操作的方式与上述无异。请看下面这个例子:
var slice2 = slice1[1:3]
据此,slice2 的值为[]int{3, 4}。注意,作为切片表达式求值结果的切片值的长度总是为元素上界索引与元素下界索引的差值。
除了长度,切片值以及数组值还有另外一个属性——容量。数组值的容量总是等于其长度。而切片值的容量则往往与其长度不同。
如图所示,一个切片值的容量即为它的第一个元素值在其底层数组中的索引值与该数组长度的差值的绝对值。为了获取数组、切片或通道类型的值的容量,我们可以使用内建函数cap,如:
var capacity2 int = cap(slice2)
最后,要注意,切片类型属于引用类型。它的零值即为 nil ,即空值。如果我们只声明一个切片类型的变量而不为它赋值,那么该变量的值将会是 nil 。例如,若有这样一个变量:
var slice3 []int
则它的值会是 nil 。
在有些时候,我们还可以在方括号中放入第三个正整数,如下所示:
numbers3[1:4:4]
这第三个正整数被称为容量上界索引。它的意义在于可以把作为结果的切片值的容量设置得更小。换句话说,它可以限制我们通过这个切片值对其底层数组中的更多元素的访问。下面举个例子。 numbers3 和 slice1 ,针对它们的赋值语句是这样的:
var numbers3 = [5]int{1, 2, 3, 4, 5}var slice1 = numbers3[1:4]
这时,变量 slice1 的值是 []int{2, 3, 4} 。但是我们可以通过如下操作将其长度延展得与其容量相同:
slice1 = slice1[:cap(slice1)]
通过此操作,变量 slice1 的值变为了 []int{2, 3, 4, 5} ,且其长度和容量均为 4 。现在,numbers3 的值中的索引值在 [1,5) 范围内的元素都被体现在了 slice1 的值中。这是以 numbers3 的值是 slice1 的值的底层数组为前提的。这意味着,我们可以轻而易举地通过切片值访问其底层数组中对应索引值更大的更多元素。如果我们编写的函数返回了这样一个切片值,那么得到它的程序很可能会通过这种技巧访问到本不应该暴露给它的元素。这是确确实实是一个安全隐患。
如果我们在切片表达式中加入了第三个索引(即容量上界索引),如:
var slice1 = numbers3[1:4:4]
那么在这之后,无论我们怎样做都无法通过 slice1 访问到 numbers3 的值中的第五个元素。因为这超出了我们刚刚设定的 slice1 的容量。如果我们指定的元素上界索引或容量上界索引超出了被操作对象的容量,那么就会引发一个运行时恐慌(程序异常的一种),而不会有求值结果返回。因此,这是一个有力的访问控制手段。
虽然切片值在上述方面受到了其容量的限制,但是我们却可以通过另外一种手段对其进行不受任何限制地扩展。这需要使用到内建函数append。append会对切片值进行扩展并返回一个新的切片值。使用方法如下:
slice1 = append(slice1, 6, 7)
通过上述操作,slice1的值变为了[]int{2, 3, 4, 6, 7}。注意,一旦扩展操作超出了被操作的切片值的容量,那么该切片的底层数组就会被自动更换。这也使得通过设定容量上界索引来对其底层数组进行访问控制的方法更加严谨了。
我们要介绍的最后一种操作切片值的方法是“复制”。该操作的实施方法是调用 copy 函数。该函数接受两个类型相同的切片值作为参数,并会把第二个参数值中的元素复制到第一个参数值中的相应位置(索引值相同)上。这里有两点需要注意:
举例如下:
var slice4 = []int{0, 0, 0, 0, 0, 0, 0}copy(slice4, slice1)
通过上述复制操作, slice4 会变为 []int{2, 3, 4, 6, 7, 0, 0} 。
Go语言的字典( Map )类型其实是哈希表( Hash Table )的一个实现。字典用于存储键-元素对(更通俗的说法是键-值对)的无序集合。
注意,同一个字典中的每个键都是唯一的。如果我们在向字典中放入一个键值对的时候其中已经有相同的键的话,那么与此键关联的那个值会被新值替换。
字典类型的字面量是 map[K]T
其中,“ K ”意为键的类型,而“ T ”则代表元素(或称值)的类型。如果我们要描述一个键类型为 int 、值类型为 string 的字典类型的话,应该这样写: map[int]string
注意,字典的键类型必须是可比较的,否则会引起错误。也就是说,它不能是切片、字典或函数类型。
字典值的字面量表示法实际上与数组和切片的字面量表示法很相似。首先,最左边仍然是类型字面量,右边紧挨着由花括号包裹且有英文逗号分隔的键值对。每个键值对的键和值之间由英文冒号分隔。以字典类型map[int]string为例,它的值的字面量可以是这样的:
map[int]string{1: "a", 2: "b", 3: "c"}
我们可以把这个值赋给一个变量:
mm := map[int]string{1: "a", 2: "b", 3: "c"}
然后运用索引表达式取出字典中的值,就像这样:
b := mm[2]
注意,在这里,我们放入方括号中的不再是索引值(实际上,字典中的键值对也没有索引),而是与我们要取出的值对应的那个键。在上例中变量b的值必是字符串" b "。当然,也可以利用索引表达式来赋值,比如这样:
mm[2] = b + "2"
这使得字典 mm 中与键 2 对应的值变为了" b2 "。现在我们再来向 mm 添加一个键值对:mm[4] = ""
之后,在从中取出与 4
和 5
对应的值:
d := mm[4]e := mm[5]
此时,变量 d 和 e 的值都会是多少呢?答案是都为"",即空字符串。对于变量 d 来说,由于在字典 mm 中与 4 对应的值就是"",所以索引表达式 mm[4] 的求值结果必为""。这理所应当。但是 mm[5] 的求值结果为什么也是空字符串呢?原因是,在 Go 语言中有这样一项规定,即:对于字典值来说,如果其中不存在索引表达式欲取出的键值对,那么就以它的值类型的空值(或称默认值)作为该索引表达式的求值结果。由于字符串类型的空值为"",所以 mm[5] 的求值结果即为""。
在不知道 mm 的确切值的情况下,我们无法得知 mm[5] 的求值结果意味着什么?它意味着 5 对应的值就是一个空字符串?还是说 mm 中根本就没有键为 5 的键值对?这无所判别。为了解决这个问题, Go 语言为我们提供了另外一个写法,即:
e, ok := mm[5]
针对字典的索引表达式可以有两个求值结果。第二个求值结果是 bool 类型的。它用于表明字典值中是否存在指定的键值对。在上例中,变量 ok 必为 false 。因为 mm 中不存在以 5 为键的键值对。
从字典中删除键值对的方法非常简单,仅仅是调用内建函数 delete 而已,就像这样:
delete(mm, 4)
无论 mm 中是否存在以 4 为键的键值对, delete 都会“无声”地执行完毕。我们用“有则删除,无则不做”可以很好地概括它的行为。
最后,与切片类型相同,字典类型属于引用类型。它的零值即为 nil 。
通道( Channel )是 Go 语言中一种非常独特的数据结构。它可用于在不同 Goroutine 之间传递类型化的数据,并且是并发安全的。相比之下,我们之前介绍的那些数据类型都不是并发安全的。这一点需要特别注意。
Goroutine(也称为 Go 程序)可以被看做是承载可被并发执行的代码块的载体。它们由 Go 语言的运行时系统调度,并依托操作系统线程(又称内核线程)来并发地执行其中的代码块。
通道类型的表示方法很简单,仅由chan T
两部分组成。
在这个类型字面量中,左边是代表通道类型的关键字 chan ,而右边则是一个可变的部分,即代表该通道类型允许传递的数据的类型(或称通道的元素类型)。
与其它的数据类型不同,我们无法表示一个通道类型的值。因此,我们也无法用字面量来为通道类型的变量赋值。我们只能通过调用内建函数 make 来达到目的。 make 函数可接受两个参数。第一个参数是代表了将被初始化的值的类型的字面量(比如 chan int ),而第二个参数则是值的长度。例如,若我们想要初始化一个长度为 5 且元素类型为 int 的通道值,则需要这样写:
make(chan int, 5)
实际上 make 函数也可以被用来初始化切片类型或字典类型的值。
确切地说,通道值的长度应该被称为其缓存的尺寸。换句话说,它代表着通道值中可以暂存的数据的个数。注意,暂存在通道值中的数据是先进先出的。
下面,我们声明一个通道类型的变量,并为其赋值:
ch1 := make(chan string, 5)
这样一来,我们就可以使用接收操作符 <-
向通道值发送数据了。当然,也可以使用它从通道值接收数据。例如,如果我们要向通道 ch1 发送字符串 “value1” ,那么应该这样做:
ch1 <- "value1"
另一方面,我们若想从ch1那里接收字符串,则要这样:
value := <- ch1
与针对字典值的索引表达式一样,针对通道值的接收操作也可以有第二个结果值。
value, ok := <- ch1
这样做的目的同样是为了消除与零值有关的歧义。这里的变量 ok 的值同样是 bool 类型的。它代表了通道值的状态, true 代表通道值有效,而 false 则代表通道值已无效(或称已关闭)。更深层次的原因是,如果在接收操作进行之前或过程中通道值被关闭了,则接收操作会立即结束并返回一个该通道值的元素类型的零值。按照上面的第一种写法,我们无从判断接收到零值的原因是什么。不过,有了第二个结果值之后,这种判断就好做了。
说到关闭通道值,我们可以通过调用内建函数 close 来达到目的,就像这样:
close(ch1)
注意,对通道值的重复关闭会引发运行时恐慌。这会使程序崩溃。所以一定要避免这种情况的发生。另外,在通道值有效的前提下,针对它的发送操作会在通道值已满(其中缓存的数据的个数已等于它的长度)时被阻塞。而向一个已被关闭的通道值发送数据会引发运行时恐慌。另一方面,针对有效通道值的接收操作会在它已空(其中没有缓存任何数据)时被阻塞。除此之外,还有几条与通道的发送和接收操作有关的规则。不过在这里我们记住上面这三条就可以了。
通道有带缓冲和非缓冲之分。我们已经说过,缓冲通道中可以缓存 N 个数据。我们在初始化一个通道值的时候必须指定这个 N 。相对的,非缓冲通道不会缓存任何数据。发送方在向通道值发送数据的时候会立即被阻塞,直到有某一个接收方已从该通道值中接收了这条数据。非缓冲的通道值的初始化方法如下:
make(chan int, 0)
注意,在这里,给予 make 函数的第二个参数值是 0 。
除了上述分类方法,我们还可以以数据在通道中的传输方向为依据来划分通道。默认情况下,通道都是双向的,即双向通道。如果数据只能在通道中单向传输,那么该通道就被称作单向通道。我们在初始化一个通道值的时候不能指定它为单向。但是,在编写类型声明的时候,我们却是可以这样做的。例如:
type Receiver <-chan int
类型 Receiver 代表了一个只可从中接收数据的单向通道类型。这样的通道也被称为接收通道。在关键字 chan 左边的接收操作符 <-
形象地表示出了数据的流向。相对应的,如果我们想声明一个发送通道类型,那么应该这样:
type Sender chan<- int
这次 <-
被放在了 chan 的右边,并且“箭头”直指“通道”。想必不用多说你也能明白了。我们可以把一个双向通道值赋予上述类型的变量,就像这样:
var myChannel = make(chan int, 3)var sender Sender = myChannelvar receiver Receiver = myChannel
但是,反之则是不行的。像下面这样的代码是通不过编译的:
var myChannel1 chan int = sender
单向通道的主要作用是约束程序对通道值的使用方式。比如,我们调用一个函数时给予它一个发送通道作为参数,以此来约束它只能向该通道发送数据。又比如,一个函数将一个接收通道作为结果返回,以此来约束调用该函数的代码只能从这个通道中接收数据。这属于 API 设计的范畴。
最后,与切片和字典类型相同,通道类型属于引用类型。它的零值即为 nil 。
在 Go 语言中,函数是一等( first-class )类型。这意味着,我们可以把函数作为值来传递和使用。函数代表着这样一个过程:它接受若干输入(参数),并经过一些步骤(语句)的执行之后再返回输出(结果)。特别的是, Go 语言中的函数可以返回多个结果。
函数类型的字面量由关键字 func 、由圆括号包裹参数声明列表、空格以及可以由圆括号包裹的结果声明列表组成。其中,参数声明列表中的单个参数声明之间是由英文逗号分隔的。每个参数声明由参数名称、空格和参数类型组成。参数声明列表中的参数名称是可以被统一省略的。结果声明列表的编写方式与此相同。结果声明列表中的结果名称也是可以被统一省略的。并且,在只有一个无名称的结果声明时还可以省略括号。示例如下:
func(input1 string ,input2 string) string
这一类型字面量表示了一个接受两个字符串类型的参数且会返回一个字符串类型的结果的函数。如果我们在它的左边加入 type 关键字和一个标识符作为名称的话,那就变成了一个函数类型声明,就像这样:
type MyFunc func(input1 string ,input2 string) string
函数值(或简称函数)的写法与此不完全相同。编写函数的时候需要先写关键字 func 和函数名称,后跟参数声明列表和结果声明列表,最后是由花括号包裹的语句列表。例如:
func myFunc(part1 string, part2 string) (result string) { result = part1 + part2 return}
我们在这里用到了一个小技巧:如果结果声明是带名称的,那么它就相当于一个已被声明但未被显式赋值的变量。我们可以为它赋值且在 return 语句中省略掉需要返回的结果值。显然,该函数还有一种更常规的写法:
func myFunc(part1 string, part2 string) string { return part1 + part2}
注意,函数 myFunc 是函数类型 MyFunc 的一个实现。实际上,只要一个函数的参数声明列表和结果声明列表中的数据类型的顺序和名称与某一个函数类型完全一致,前者就是后者的一个实现。请大家回顾上面的示例并深刻理解这句话。
我们可以声明一个函数类型的变量,如:
var splice func(string, string) string // 等价于 var splice MyFunc
然后把函数myFunc赋给它:
splice = myFunc
如此一来,我们就可以在这个变量之上实施调用动作了:
splice("1", "2")
实际上,这是一个调用表达式。它由代表函数的标识符(这里是 splice )以及代表调用动作的、由圆括号包裹的参数值列表组成。
如果你觉得上面对 splice 变量声明和赋值有些啰嗦,那么可以这样来简化它:
var splice = func(part1 string, part2 string) string { return part1 + part2}
在这个示例中,我们直接使用了一个匿名函数来初始化 splice 变量。顾名思义,匿名函数就是不带名称的函数值。匿名函数直接由函数类型字面量和由花括号包裹的语句列表组成。
注意,这里的函数类型字面量中的参数名称是不能被忽略的。
其实,我们还可以进一步简化——索性省去 splice 变量。既然我们可以在代表函数的变量上实施调用表达式,那么在匿名函数上肯定也是可行的。因为它们的本质是相同的。后者的示例如下:
var result = func(part1 string, part2 string) string { return part1 + part2}("1", "2")
可以看到,在这个匿名函数之后的即是代表调用动作的参数值列表。注意,这里的 result 变量的类型不是函数类型,而与后面的匿名函数的结果类型是相同的。
最后,函数类型的零值是 nil 。这意味着,一个未被显式赋值的、函数类型的变量的值必为 nil 。
Go 语言的结构体类型( Struct )比函数类型更加灵活。它可以封装属性和操作。前者即是结构体类型中的字段,而后者则是结构体类型所拥有的方法。
结构体类型的字面量由关键字 type 、类型名称、关键字 struct ,以及由花括号包裹的若干字段声明组成。其中,每个字段声明独占一行并由字段名称(可选)和字段类型组成。示例如下:
type Person struct { Name string Gender string Age uint8}
结构体类型 Person 中有三个字段,分别是 Name 、 Gender 和 Age 。我们可以用字面量创建出一个该类型的值,像这样:
Person{Name: "Robert", Gender: "Male", Age: 33}
可以看到,结构体值的字面量(或简称结构体字面量)由其类型的名称和由花括号包裹的若干键值对组成。
注意,这里的键值对与字典字面量中的键值对的写法相似,但不相同。这里的键是其类型中的某个字段的名称(注意,它不是字符串字面量),而对应的值则是欲赋给该字段的那个值。另外,如果这里的键值对的顺序与其类型中的字段声明完全相同的话,我们还可以统一省略掉所有字段的名称,就像这样:
Person{"Robert", "Male", 33}
当然,我们在编写某个结构体类型的值字面量时可以只对它的部分字段赋值,甚至不对它的任何字段赋值。这时,未被显式赋值的字段的值则为其类型的零值。注意,在上述两种情况下,字段的名称是不能被省略的。
与代表函数值的字面量类似,我们在编写一个结构体值的字面量时不需要先拟好其类型。这样的结构体字面量被称为匿名结构体。与匿名函数类似,我们在编写匿名结构体的时候需要先写明其类型特征(包含若干字段声明),再写出它的值初始化部分。下面,我们依照结构体类型 Person 创建一个匿名结构体:
p := struct { Name string Gender string Age uint8}{"Robert", "Male", 33}
匿名结构体最大的用处就是在内部临时创建一个结构以封装数据,而不必正式为其声明相关规则。而在涉及到对外的场景中,强烈建议使用正式的结构体类型。
结构体类型可以拥有若干方法(注意,匿名结构体是不可能拥有方法的)。所谓方法,其实就是一种特殊的函数。它可以依附于某个自定义类型。方法的特殊在于它的声明包含了一个接收者声明。这里的接收者指代它所依附的那个类型。我们仍以结构体类型 Person 为例。下面是依附于它的一个名为 Grow 的方法的声明:
func (person *Person) Grow() { person.Age++}
如上所示,在关键字 func 和名称 Grow 之间的那个圆括号及其包含的内容就是接收者声明。其中的内容由两部分组成。第一部分是代表它依附的那个类型的值的标识符。第二部分是它依附的那个类型的名称。后者表明了依附关系,而前者则使得在该方法中的代码可以使用到该类型的值(也称为当前值)。代表当前值的那个标识符可被称为接收者标识符,或简称为接收者。请看下面的示例:
p := Person{"Robert", "Male" 33}p.Grow()
我们可以直接在 Person 类型的变量 p 之上应用调用表达式来调用它的方法 Grow 。注意,此时方法 Grow 的接收者标识符 person 指代的正是变量 p 的值。这也是“当前值”这个词的由来。在 Grow 方法中,我们通过使用选择表达式选择了当前值的字段 Age ,并使其自增。因此,在语句 p.Grow() 被执行之后, p 所代表的那个人就又年长了一岁( p 的 Age 字段的值已变为 34 )。
需要注意的是,在 Grow 方法的接收者声明中的那个类型是 *Person ,而不是 Person 。实际上,前者是后者的指针类型。这也使得 person 指代的是 p 的指针,而不是它本身。
说到这里,熟悉面向对象编程的同学可能已经意识到,包含若干字段和方法的结构体类型就相当于一个把属性和操作封装在一起的对象。不过要注意,与对象不同的是,结构体类型(以及任何类型)之间都不可能存在继承关系。实际上,在 Go 语言中并没有继承的概念。
最后,结构体类型属于值类型。它的零值并不是 nil ,而是其中字段的值均为相应类型的零值的值。举个例子,结构体类型 Person 的零值若用字面量来表示的话则为 Person{} 。
在 Go 语言中,一个接口类型总是代表着某一种类型(即所有实现它的类型)的行为。一个接口类型的声明通常会包含关键字 type 、类型名称、关键字 interface 以及由花括号包裹的若干方法声明。示例如下:
type Animal interface { Grow() Move(string) string}
注意,接口类型中的方法声明是普通的方法声明的简化形式。它们只包括方法名称、参数声明列表和结果声明列表。其中的参数的名称和结果的名称都可以被省略。不过,出于文档化的目的,我还是建议大家在这里写上它们。因此, Move 方法的声明至少应该是这样的:
Move(new string) (old string)
如果一个数据类型所拥有的方法集合中包含了某一个接口类型中的所有方法声明的实现,那么就可以说这个数据类型实现了那个接口类型。所谓实现一个接口中的方法是指,具有与该方法相同的声明并且添加了实现部分(由花括号包裹的若干条语句)。相同的方法声明意味着完全一致的名称、参数类型列表和结果类型列表。其中,参数类型列表即为参数声明列表中除去参数名称的部分。一致的参数类型列表意味着其长度以及顺序的完全相同。对于结果类型列表也是如此。
*Person 类型(注意,不是Person类型)拥有一个 Move 方法。该方法会是Animal接口的 Move 方法的一个实现。再加上我们在之前为它编写的那个 Grow 方法,*Person类型就可以被看做是 Animal 接口的一个实现类型了。
你可能已经意识到,我们无需在一个数据类型中声明它实现了哪个接口。只要满足了“方法集合为其超集”的条件,就建立了“实现”关系。这是典型的无侵入式的接口实现方法。
好了,现在我们已经认为 *Person 类型实现了 Animal 接口。但是 Go 语言编译器是否也这样认为呢?这显然需要一种显式的判定方法。在 Go 语言中,这种判定可以用类型断言来实现。不过,在这里,我们是不能在一个非接口类型的值上应用类型断言来判定它是否属于某一个接口类型的。我们必须先把前者转换成空接口类型的值。这又涉及到了 Go 语言的类型转换。
Go 语言的类型转换规则定义了是否能够以及怎样可以把一个类型的值转换另一个类型的值。另一方面,所谓空接口类型即是不包含任何方法声明的接口类型,用 interface{} 表示,常简称为空接口。正因为空接口的定义,Go 语言中的包含预定义的任何数据类型都可以被看做是空接口的实现。我们可以直接使用类型转换表达式把一个*Person类型转换成空接口类型的值,就像这样:
p := Person{"Robert", "Male", 33, "Beijing"}v := interface{}(&p)
请注意第二行。在类型字面量后跟由圆括号包裹的值(或能够代表它的变量、常量或表达式)就构成了一个类型转换表达式,意为将后者转换为前者类型的值。在这里,我们把表达式 &p 的求值结果转换成了一个空接口类型的值,并由变量 v 代表。
注意,表达式 &p( & 是取址操作符)的求值结果是一个 *Person 类型的值,即 p 的指针。
在这之后,我们就可以在 v 上应用类型断言了,即:
h, ok := v.(Animal)
类型断言表达式 v.(Animal) 的求值结果可以有两个。第一个结果是被转换后的那个目标类型(这里是 Animal )的值,而第二个结果则是转换操作成功与否的标志。显然, ok 代表了一个 bool 类型的值。它也是这里判定实现关系的重要依据。
至此,我们掌握了接口类型、实现类型以及实现关系判定的重要知识和技巧。
我们在前面多次提到过指针及指针类型。例如,*Person 是Person的指针类型。又例如,表达式 &p 的求值结果是 p 的指针。方法的接收者类型的不同会给方法的功能带来什么影响?该方法所属的类型又会因此发生哪些潜移默化的改变?
指针操作涉及到两个操作符—— & 和 *。这两个操作符均有多个用途。但是当它们作为地址操作符出现时,前者的作用是取址,而后者的作用是取值。更通俗地讲,当地址操作符 & 被应用到一个值上时会取出指向该值的指针值,而当地址操作符 * 被应用到一个指针值上时会取出该指针指向的那个值。它们可以被视为相反的操作。
除此之外,当 * 出现在一个类型之前(如 *Person 和 *[3]string )时就不能被看做是操作符了,而应该被视为一个符号。如此组合而成的标识符所表达的含义是作为第二部分的那个类型的指针类型。我们也可以把其中的第二部分所代表的类型称为基底类型。例如,*[3]string 是数组类型 [3]string 的指针类型,而 [3]string 是 *[3]string 的基底类型。
好了,我们现在回过头去再看结构体类型 Person 。它及其两个方法的完整声明如下:
type Person struct { Name string Gender string Age uint8 Address string}func (person *Person) Grow() { person.Age++}func (person *Person) Move(newAddress string) string { old := person.Address person.Address = newAddress return old}
注意, Person 的两个方法 Grow 和 Move 的接收者类型都是 *Person,而不是 Person。只要一个方法的接收者类型是其所属类型的指针类型而不是该类型本身,那么我就可以称该方法为一个指针方法。上面的 Grow 方法和 Move 方法都是 Person 类型的指针方法。
相对的,如果一个方法的接收者类型就是其所属的类型本身,那么我们就可以把它叫做值方法。我们只要微调一下 Grow 方法的接收者类型就可以把它从指针方法变为值方法:
func (person Person) Grow() { person.Age++}
那指针方法和值方法到底有什么区别呢?我们在保留上述修改的前提下编写如下代码:
p := Person{"Robert", "Male", 33, "Beijing"}p.Grow()fmt.Printf("%v\n", p)
这段代码被执行后,标准输出会打印出什么内容呢?直觉上, 34 会被打印出来,但是被打印出来的却是 33 。这是怎么回事呢? Grow 方法的功能失效了?!
解答这个问题需要引出一条定论:方法的接收者标识符所代表的是该方法当前所属的那个值的一个副本,而不是该值本身。例如,在上述代码中, Person 类型的 Grow 方法的接收者标识符 person 代表的是 p 的值的一个拷贝,而不是 p 的值。我们在调用 Grow 方法的时候, Go 语言会将 p 的值复制一份并将其作为此次调用的当前值。正因为如此, Grow 方法中的 person.Age++ 语句的执行会使这个副本的 Age 字段的值变为 34 ,而 p 的 Age 字段的值却依然是33。这就是问题所在。
只要我们把 Grow 变回指针方法就可以解决这个问题。原因是,这时的 person 代表的是 p 的值的指针的副本。指针的副本仍会指向 p 的值。另外,之所以选择表达式 person.Age 成立,是因为如果 Go 语言发现 person 是指针并且指向的那个值有 Age 字段,那么就会把该表达式视为( *person).Age 。其实,这时的 person.Age 正是 (*person).Age 的速记法。
如果一个数据类型所拥有的方法集合中包含了某一个接口类型中的所有方法声明的实现,那么就可以说这个数据类型实现了那个接口类型。要获知一个数据类型都包含哪些方法并不难。但是要注意指针方法与值方法的区别。
拥有指针方法 Grow 和 Move 的指针类型 *Person 是接口类型 Animal 的实现类型,但是它的基底类型 Person 却不是。这样的表象隐藏着另一条规则:一个指针类型拥有以它以及以它的基底类型为接收者类型的所有方法,而它的基底类型却只拥有以它本身为接收者类型的方法。
以 Person 类型为例。即使我们把 Grow 和 Move 都改为值方法,*Person 类型也仍会是 Animal 接口的实现类型。另一方面, Grow 和 Move 中只要有一个是指针方法, Person 类型就不可能是 Animal 接口的实现类型。
另外,还有一点需要大家注意,我们在基底类型的值上仍然可以调用它的指针方法。例如,若我们有一个 Person 类型的变量 bp ,则调用表达式 bp.Grow() 是合法的。这是因为,如果 Go 语言发现我们调用的 Grow 方法是 bp 的指针方法,那么它会把该调用表达式视为 (&bp).Grow() 。实际上,这时的 bp.Grow() 是 (&bp).Grow() 的速记法。
本篇结束,篇幅较长,请看下一篇
(完)
]]>研究了一下 golang 相关的文档,写下这篇 golang 入门日记,包含 golang 基础教程,这是 golang 语言初探第一篇。
周末的时候研究了一下 golang 相关的文档,根据自己的工作经验,我觉得这门语言在未来一定会大放异彩,其实现在也比较热门了。主要有以下几个特点:
经过对 golang 的这些了解,顿时感觉有点兴趣了,在这里记录一些入门的资料。
go env -w GO111MODULE=ongo env -w GOPROXY=https://goproxy.cn,direct
任何Go语言源码文件都由若干个程序实体组成的。在 Go 语言中,变量、常量、函数、结构体和接口被统称为“程序实体”,而它们的名字被统称为“标识符”。
标识符可以是任何 Unicode 编码可以表示的字母字符、数字以及下划线“_”。不过,首字母不能是数字或下划线。
注意:在 Go 语言中,我们对程序实体的访问权限控制只能通过它们的名字来实现。名字首字母为大写的程序实体可以被任何代码包中的代码访问到。而名字首字母为小写的程序实体则只能被同一个代码包中的代码所访问。
Go 语言还规定了一些特定的字符序列。它们被称为“关键字”。编程人员不能把关键字作为标识符。 Go 语言的关键字如下图:
用于声明变量的关键字 var ,以及用于声明常量的关键字 const 。
var num1 int = 1 var num2, num3 int = 2, 3 // 注释:平行赋值 var ( // 注释:多行赋值 num4 int = 4 num5 int = 5)
要注意,对于常量不能出现只声明不赋值的情况。
Go语言的整数类型一共有10个。
其中计算架构相关的整数类型有两个,即:有符号的整数类型 int 和无符号的整数类型 uint 。
有符号的整数类型会使用最高位的比特(bit)表示整数的正负。显然,这会对它能表示的整数的范围有一定的损耗(使其缩小)。而无符号的整数类型会使用所有的比特位来表示数值。如此类型的值均为正数。这也是用“无符号的”来形容它们的原因。
在不同的计算架构的计算机之上,它们体现的宽度是不同的。请看下表。
除了这两个计算架构相关的整数类型之外,还有8个可以显式表达自身宽度的整数类型。如下表所示。
它们的宽度意味着其自身的范围:
除十进制外,还有八进制、十六进制的表示方法
var num1 int = 12num1 = 014 // 用“0”作为前缀以表明这是8进制表示法。num1 = 0xC // 用“0x”作为前缀以表明这是16进制表示法。
浮点数类型有两个,即 float32 和 float64 。存储这两个类型的值的空间分别需要 4 个字节和 8 个字节。
浮点数类型的值一般由整数部分、小数点“.”和小数部分组成。其中,整数部分和小数部分均由 10 进制表示法表示。不过还有另一种表示方法。那就是在其中加入指数部分。指数部分由“ E ”或“ e ”以及一个带正负号的 10 进制数组成。比如,3.7E-2 表示浮点数 0.037 。又比如,3.7E+1 表示浮点数 37 。
有时候,浮点数类型值的表示也可以被简化。比如,37.0 可以被简化为 37 。又比如, 0.037 可以被简化为 .037 。
有一点需要注意,在 Go 语言里,浮点数的相关部分只能由 10 进制表示法表示,而不能由 8 进制表示法或 16 进制表示法表示。比如,03.7 表示的一定是浮点数 3.7 。
复数类型同样有两个,即 complex64 和 complex128 。存储这两个类型的值的空间分别需要 8 个字节和 16 个字节。实际上,complex64 类型的值会由两个 float32 类型的值分别表示复数的实数部分和虚数部分。而 complex128 类型的值会由两个 float64 类型的值分别表示复数的实数部分和虚数部分。
复数类型的值一般由浮点数表示的实数部分、加号“+”、浮点数表示的虚数部分,以及小写字母“i”组成。比如,3.7E+1 + 5.98E-2i。正因为复数类型的值由两个浮点数类型值组成,所以其表示法的规则自然需遵从浮点数类型的值表示法的相关规则。
var num3 = 3.7E+1 + 5.98E-2i// 37+0.0598i
byte 与 rune 类型有一个共性,即:它们都属于别名类型。 byte 是 uint8 的别名类型,而 rune 则是 int32 的别名类型。
一个 rune 类型的值即可表示一个 Unicode 字符。 Unicode 是一个可以表示世界范围内的绝大部分字符的编码规范。关于它的详细信息,大家可以参看其官网(http://unicode.org/)上的文档,或在 Google 上搜索。用于代表 Unicode 字符的编码值也被称为 Unicode 代码点。一个 Unicode 代码点通常由“ U+ ”和一个以十六进制表示法表示的整数表示。例如,英文字母“ A ”的 Unicode 代码点为“ U+0041 ”。
rune 类型的值需要由单引号“ ’ ”包裹。例如,’ A ‘或’ 郝 '。这种表示方法一目了然。不过,我们还可以用另外几种形式表示 rune 类型值。
另外,在 rune 类型值的表示中支持几种特殊的字符序列,即:转义符。它们由“ \ ”和一个单个英文字符组成。
一个字符串类型的值可以代表一个字符序列。这些字符必须是被 Unicode 编码规范支持的。虽然从表象上来说是字符序列,但是在底层,一个字符串值却是由若干个字节来表现和存储的。一个字符串(也可以说字符序列)会被 Go 语言用 Unicode 编码规范中的 UTF-8 编码格式编码为字节数组。
注意,我们在一个字符串值或者一个字符串类型的变量之上应用 Go 语言的内置函数 len 将会得到代表它的那个字节数组的长度。这可能与我们看到的表象是不同的。
字符串的表示法有两种,即:原生表示法和解释型表示法。若用原生表示法,需用反引号“ ` ”把字符序列包裹起来。若用解释型表示法,则需用双引号“ " ”包裹字符序列。
二者的区别是,前者表示的值是所见即所得的(除了回车符)。在那对反引号之间的内容就是该字符串值本身。而后者所表示的值中的转义符会起作用并在程序编译期间被转义。所以,如此表示的字符串值的实际值可能会与我们看到的表象不相同。
本篇结束,篇幅较长,请看下一篇
(完)
]]>这是原作者经历创业后的一些反思,觉得很好,转载至此。原文链接: http://jafeney.com/2017/08/03/20170803/
大众创业,万众创新。从大学开始一直有个创业的梦想,去年也终于打开步子去做了。然而这一年下来,从0到1 感受了太大的心酸苦楚,有时克服一个困难紧接着新的麻烦又会接踵而至。在这种高压下奔跑是怎样一种感受,只有经历过的人才会懂。
杭州每年都上千个互联网注册,同样也有上百个公司倒闭,以前一直觉得是骇人听闻,现在也渐渐明白过来。创业不是件容易的事,没有任何人,任何理由让你一定能成功,更多的则是失败。细细回想,下面这些对互联网型创业至关重要:
创业绝对不是一个的战斗,做任何事情都需要一个Team,而对于互联网型创业,最好的合伙人组合就是 一个有领导能力和全局观的CEO,一个技术精湛善于团队管理的CTO,一个对市场和运营颇有经验的COO。有了这个互补的铁三角,如果再找到一个财大气粗的投资方,那前期的工作会非常顺利。
上面说的四者关系,任何一个缺失都会导致相应的问题。很多公司初创时都不太可能集齐这4颗龙珠,而一般的做法都是身兼数职,现学现卖。比如我们公司,我和我的CEO都是技术出身,尤其我的CEO对技术还特别痴迷,遇到技术难题经常喜欢钻牛角尖,好在都能钻出解决方案来,但一来二去也会花费大量的时间。而对技术痴迷的人 往往大局观比较差,尤其是项目规划能力。这也是 为什么技术专家很少 能做一个合格的CEO,而一个牛逼的CEO到后来也不做一线开发一样。
至于怎么办?我觉得前期可以身兼数职,现学现卖成功的人也不少。但如果做不好或达不到要求,还是趁早去招聘合适的人比较好,千万不要瞎搞,公司初创期越折腾越容易夭折。
公司股权是公司的核心,领导班子的股权分配决定一个公司的发展方向。我觉得最合理的比例应该是CEO占主导权,其他几位合伙人根据出资情况占股。对,必须实际出资,千万别搞什么干股,或者技术入股啥的。人都是一样,花的不是自己的钱,就不会真正珍惜。一旦公司出现危机情况,就容易置身事外,或者撂挑子跑路。
如果创业初期没有一个靠谱的、且可以持续赢利的项目,那没必要创业,公司没有收入就没有存在的必要。除非你的商业计划书得到了不错的风险投资,允许公司至少3个月不盈利,但压力会越来越大。
值得一提的是,这种靠风头起家而一直没有可持续赢利的项目公司很容易丧失主导权,容易被投资方牵着鼻子走,甚至成为投资人的狗腿子。公司一旦丧失主导权,领导班子在员工面前就容易丧失权威。作为CEO,你应该时时刻刻让员工明白到底是在为谁打工,树立自己的权威,能高效调动手底下一切力量,这是企业的灵魂。
作为CEO,必须要有大局观,对公司发展和项目的生命周期要有一个清晰的规划。这绝不是写个商业计划书那么简单,实际推进的过程中会遇到各式各样的问题,这些问题很可能打断公司原有的发展计划,这个时候就需要一个清晰的大脑来调整方案,重新规划,而不是继续埋头苦干,闭门造车。
很多人会有这样的感觉,创业只要努力就能取得成功。事实上没有一个创业者是不努力的,加班加点也是家常便饭,但真正取得成功的公司却不在多数。创业不是埋头苦干,而是找对方向做对的事。
说得具体点,我们在创业的时候应该阶段性地做反思和讨论,对成功的要肯定,对失败的要批评和改正。领导班子内部也要经常开讨论会,沟通有无很重要。对于未知的事情,多几次头脑风暴,多几个角度思考问题,调整方向,这对事情取得成功越有帮助。
创业最大的忌讳就是痴迷存量,而不敢去拥抱变化。趋利避害,见好就收,这其实是人的天性。从某种意义上来说,创业比较像赌博,敢于冒险的人能赢大钱(当然运气不好也会千家荡产),而想闷声发财的人往往赢不到大钱。so,那些不肯下注却老想着怎么赢大钱的人,无疑是在痴人说梦。记住,世上从来没有免费的午餐,不要深信任何人(至亲除外)。换句话说。这样的人本身也不适合创业,老老实实找个安生地妥妥地上班可能更适合。
总结地说,创业如果痴迷存量带来的好处,不敢去面对增量带来的麻烦,那就容易原地踏步,迟早会被后浪或者同行拍死在沙滩上。
创业为什么能成功?细细想来——那就是身边所有的人都愿意帮你,你整合资源的成本变得非常非常地低,你才可能成功。这是一个很简单的道理,所以沿途去获取帮助非常得重要。这种能力和创业者人品息息相关,所以商人一般特别在意信誉和口碑,获取身边人的认可你的产品就成功了一半。
(完)
]]>一篇关于个人收藏的导航文章。
个人的大多数收藏都是通过谷歌的书签收藏,但访问谷歌又不是那么方便,而且找的时候也有点难找,有的时候想快速查点东西,很麻烦。
对这些资源进行分类是挺麻烦的,就按照内容类型区分吧,不定期整理,表格排名不分先后。
名字 | 链接 | 简介 | 标签 | 备注 |
---|---|---|---|---|
干货集中营 | https://gank.io/ | 偏前端社区 | 全栈,app,flutter | |
码力全开 | https://www.maliquankai.com/ | 自由职业 | app | 可以在上面找资料 |
名字 | 链接 | 简介 | 标签 | 备注 |
---|---|---|---|---|
风雪之隅 | https://www.laruence.com/ | Laruence | php,Laruence | |
岁寒 | https://lvwenhan.com/ | 全栈开发 | php,laravel,iOS | |
施国鹏 seth-shi 的博客 | https://www.shiguopeng.cn/ | 全栈开发 | php,golang | |
jafeney 的博客 | http://jafeney.com/ | 前端开发 | 前端 | |
晚晴幽草轩 | https://www.jeffjade.com/ | 好多互联网内容 | 各种各样 | 没事可以逛逛,激发灵感 ,里面也有一些收集资料 |
贾鹏辉的博客 | https://www.devio.org/ | 移动端开发 | flutter,iOS,Android | |
蒋继发的博客 | https://thaddeusjiang.com/ | 移动端开发 | 前端 | |
云风的博客 | https://blog.codingnow.com/ | 移动端开发 | lua,游戏设计 |
名字 | 链接 | 简介 | 标签 | 备注 |
---|---|---|---|---|
HTTP 接口设计指北 | https://github.com/bolasblack/http-api-guide | api设计 | http,api | |
你真的了解React Hooks吗? | https://tech.youzan.com/hookhe-lei-zu-jian-zai-shi-yong-shang-you-he-bu-tong/ | react hooks深入了解 | react,hooks | |
前端测试 | https://gamehu.github.io/2020/07/20/前端测试/ | 单元测试,集成测试,UI测试 | react,hooks |
名字 | 链接 | 简介 | 标签 | 备注 |
---|---|---|---|---|
https://www.pinterest.com/ | 高清图片 | picture,idea | 可以去上面逛逛找灵感 | |
logo | https://hatchful.shopify.com/zh-CN/ | 快速制作logo | logo,idea | |
正版高清图片 | https://pixabay.com/zh/ | 正版高清图片 | picture |
名字 | 链接 | 简介 | 标签 | 备注 |
---|---|---|---|---|
验证码接收 | https://www.yinsiduanxin.com/ | 验证码接收平台 | 验证码 | |
图片转字符 | https://www.degraeve.com/img2txt.php | 图片转字符 | ||
vultr | https://www.vultr.com/?ref=8868444-6G | 国外服务器 | ||
shadowsocks | https://portal.shadowsocks.nz/aff.php?aff=36748 | 翻墙服务 |
(完)
]]>Omega 是 hexo 框架下的一个博客主题。
使用过各种各样的工具、框架搭建博客,在去年最终选择使用 hexo 来搭建,主要是不用维护服务器,简单方便,然而始终没有找到称心如意的主题。所以决定动手自己做一个。
正如其名,我希望有一个纯粹的、便于阅读、容易定制化的博客主题。现在有很多 hexo 的主题框架,但是都不如我意,这也就是我为什么要做这个主题的初衷。主题参考了 Hux 的博客整体布局,我也比较喜欢这种布局。
主题是基于 hexo 框架的,首先我希望博客不需要太多复杂、混乱、无用的功能。我理想的博客就是一个记录生活、提供与志同道合的网友的交流平台。
博客最主要的功能就是提供阅读了,所以一个良好的阅读体验是最重要的。
如果博客和别人的完全一致,那也不好,不够独特,容易审美疲劳。所以主题具有定制化的功能是必须的。
但是定制化如果过于复杂,又会增加时间成本,就变得如同开发一个主题了,所以简化定制化成本也是必须的。
将主题文件放置在 themes 目录,修改根目录下的配置文件 _config.yml 的 theme 为 omega
主题有以下依赖,尽量保持一致,否则可能会出现未知问题。
"dependencies": { "cheerio": "^1.0.0-rc.6", "hexo": "^4.0.0", "hexo-generator-archive": "^1.0.0", "hexo-generator-category": "^1.0.0", "hexo-generator-feed": "^2.2.0", "hexo-generator-index-pin-top": "^0.2.2", "hexo-generator-searchdb": "^1.3.3", "hexo-generator-seo-friendly-sitemap": "0.0.25", "hexo-generator-tag": "^1.0.0", "hexo-renderer-ejs": "^1.0.0", "hexo-renderer-less": "^2.0.2", "hexo-renderer-markdown-it": "^3.4.1", "hexo-server": "^1.0.0", "hexo-tag-cloud": "^2.1.2", "hexo-translate-title": "^1.0.11", "hexo-wordcount": "^6.0.1" }
根目录下的 _config.yml 配置可参考我的博客,主题下的配置变量主要有以下内容:
# omega setting start# ----------------------------------------------------------# Site settingsbeian: 赣ICP备17009879号SEOTitle: kavience | Blog# ----------------------------------------------------------# Banner settingsbanner: true # close all header banner if falseheader_img: /img/header_img/home.jpgarticle_img: /img/header_img/article-bg.jpgarchives_img: /img/header_img/archive.jpgarchive_tag_img: /img/header_img/archive-tag.jpgarchive_category_img: /img/header_img/archive-category.jpg404_img: /img/header_img/404.jpg# ----------------------------------------------------------# Contact settingsemail: kavience@gmail.comzhihu_username: kavience-xiaofangithub_username: kaviencetwitter_username: Mr_Kavience# weibo_username: xxx# facebook_username: xxx# linkedin_username: xxxsitemapIcon: trueRSS: true# ----------------------------------------------------------# Comment settingscomment_method: gitalk # gitalk or disqus## Method 1: gitalk https://github.com/gitalk/gitalkgitalk: clientID: 'd25cda615a572205be1e' clientSecret: 'faa4cf10f1daea3d4701f8be90d80feb88c818ac' repo: 'blog' owner: 'kavience' admin: ['kavience'] distractionFreeMode: false## Method 2: disqus# disqus_username: kavience# ----------------------------------------------------------# Analytics settings## Baidu Analytics# ba_track_id: xxx## Google Analyticsga_track_id: "UA-171834825-2"ga_domain: www.kavience.com# ----------------------------------------------------------# Reward settingsreward: truereward_comment: 赞赏一下wechatpay: /img/reward/wechat.pngalipay: /img/reward/alipay.png# ----------------------------------------------------------# Sidebar settingssidebar_about_description: "种一棵树最好的时间是十年前,其次是现在。"sidebar_avatar: /img/avatar/my-avatar.jpgwidgets: - short-about - recent-posts - category - archive - featured-tags - copyright# ----------------------------------------------------------# less varsless: options: globalVars: bg-color: '#f1e5c9' main-bg-color: '#f6f8fa' text-main-color: '#404046' text-secondary-color: '#a3a3a3' text-threed-color: '#808080' hover-color: '#0085a1' border-color: gray ## highlight vars # highlight-background: "#f6f8fa" # highlight-current-line: "#efefef" # highlight-selection: "#d6d6d6" # highlight-foreground: "#24292e" # highlight-comment: "#8e908c" # highlight-red: "#c82829" # highlight-orange: "#f5871f" # highlight-yellow: "#eab700" # highlight-green: "#718c00" # highlight-aqua: "#3e999f" # highlight-blue: "#4271ae" # highlight-purple: "#8959a8" highlight-background: "#002451" highlight-current-line: "#00346e" highlight-selection: "#003f8e" highlight-foreground: "#ffffff" highlight-comment: "#7285b7" highlight-red: "#ff9da4" highlight-orange: "#ffc58f" highlight-yellow: "#ffeead" highlight-green: "#d1f1a9" highlight-aqua: "#99ffff" highlight-blue: "#bbdaff" highlight-purple: "#ebbbff"
在主题开发的过程中,踩了很多坑,一方面是因为 hexo 官方的文档实在是太简陋了,感觉很多重要的信息都是一笔带过,反复看了几遍。这里把几个重要的知识点记录一下。
hexo 支持多种 css 预处理器,例如 sass、less、stylus 等,需要安装对应的渲染插件,例如 hexo-renderer-less 、hexo-renderer-stylus 。
在引入的过程,使用 css 辅助函数像这样引入就可以了。
<%- css('css/omega')%>
会自动引入主题下的 source 下的 css 目录下的 omega.css 文件,hexo-renderer-less 在打包的时候,会自动编译 omega.less 成 omega.css。
这里还有个坑,在 stylus 中可以使用 hexo-config 辅助函数获取 hexo 的配置变量,在 less 中,hexo-config 辅助函数是不存在的,只能把变量定义在 less 下面的配置,例如这样:
less: options: globalVars: bg-color: '#f1e5c9'
在开发归档的页面过程中,发现生成的 tag、category 都归属于 archive ,也就是说例如你想访问一个分类下的所有文章,像这样 https://www.kavience.com/categories/frontend/, 最终生成的这个页面采用的是 archive 模板。官网文档是这样描述的:
模板决定了网站内容的呈现方式,每个主题至少都应包含一个 index 模板,以下是各页面相对应的模板名称:
模板 | 用途 | 回退 |
---|---|---|
index | 首页 | |
post | 文章 | index |
page | 分页 | index |
archive | 归档 | index |
category | 分类归档 | archive |
tag | 标签归档 | archive |
所以必须在 archive 模板中处理这三个页面渲染的方式,可以通过 page 变量下的 category 、tag 来判断到底是属于何种归档。
博客采用的一些插件可能会导致一些奇奇怪怪的问题出现,所以请仔细看下面的说明。
(完)
]]>使用 eslint 和 prettie 再配合 husky 提高前端开发规范。
平时的开发中,开发规范必不可少,手动修改规范既不可靠,也非常繁琐,所以可以利用 eslint 和 prettie 自动修复以及规范化代码。
再配合 husky ,当开发者 commit 的时候,会自动校验,且尝试自动修复代码,一旦修复失败,则会放弃代码提交。
安装以下几个依赖:
// package.json{ // ... // 忽略其他代码 "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "lint": "lint-staged" }, "dependencies": { "eslint": "^7.25.0", "eslint-config-airbnb": "^18.2.1", "eslint-config-prettier": "^8.3.0", "eslint-import-resolver-alias": "^1.1.2", "eslint-plugin-import": "^2.22.1", "eslint-plugin-jsx-a11y": "^6.4.1", "eslint-plugin-prettier": "^3.4.0", "eslint-plugin-react": "^7.23.2", "husky": "^6.0.0", "lint-staged": "^10.5.4", "module-alias": "^2.2.2", "prettier": "^2.2.1" }, "husky": { "hooks": { "pre-commit": "lint-staged" } }, "lint-staged": { "**/*.js": [ "eslint --fix", "git add" ] }, "_moduleAliases": { "@root": ".", "@lib": "./lib" }}
配置 .eslintrc.js
module.exports = { // 使用 airbnb, prettier 推荐的规范 extends: ['airbnb', 'plugin:prettier/recommended'], parserOptions: { // 使用 es6 ecmaVersion: 6, sourceType: 'module', }, // 自定义规则 rules: { 'arrow-body-style': 0, strict: 0, 'no-console': 0, 'func-names': 0, 'space-before-function-paren': 0, 'no-param-reassign': 0, 'import/no-dynamic-require': 0, 'global-require': 0, 'consistent-return': 0, }, // 配置别名 settings: { 'import/resolver': { alias: { map: [['@', '.']], extensions: ['.js'], }, }, },};
配置好之后,安装 vscode 插件 eslint,这样可以在项目编译前检查错误,另外需要重启 vscode 使配置生效。
接下来配置 husky,安装依赖后运行以下命令:
# husky v6 版本是需要先安装的,会在项目下生成 .husky 目录node_modules/.bin/husky install# 添加 pre-commit 钩子node_modules/.bin/husky set .husky/pre-commit "npm run lint"
到此为止,项目就配置完了,接下来可以测试一下,修改一个文件,然后 commit 的时候,就会自动运行 npm run lint 了。
!注意如果 husky 没生效,一定要确认 git 是在 node_modules/.bin/husky set .husky/pre-commit “npm run lint” 之前初始化的。
添加项目规范,可以保证项目的格式统一,养成良好的开发习惯。
(完)
]]>再造一遍像 ant-design 那样的轮子库,知其然,也要知其所以然。
经过昨天的打击,今天重振旗鼓,我要重新振作起来,不就是组件库吗?既然我思路不够,那我就依照现有项目,重新造一个呗!说干就干。
先制定一下项目计划,整体年底左右完成即可,3 个月一个周期,预计 3 个周期。项目历程分为:
项目计划制定并不是一成不变,是灵活调度(偷懒用的)的,具体的 TODO 更新到我的 iPad 上吧。
计划代号为426计划,我发现很多项目都以动物命名,动物名字都快被榨干了,我就将项目名称暂定为: willow-design (柳树)。
如果按照这个项目制定计划走一遍并认真完成,是可以学习到很多东西的,希望自己能坚持下去。
写了一个脚手架:https://github.com/kavience/willow-component-tool,快速搭建 react 组件的,基于 https://github.com/kavience/willow-component-template 搭建模板,然后几个月前写了几个小组件
还是没有坚持下去,有点复杂,尤其是写文档是件很费时间的事情,终于明白为什么别人说写书是件很痛苦的事了。
更新于 2021.12.17
(完)
受到了一次【物理】打击,引发的思考。
本来打算做一个 Mas OS 的 demo,本以为会是件很简单的事,结果自从基本架子搭起来以后,一方面要找素材,另一方面要从零开始造轮子。两件事都非常折磨人,没有太多找素材的经验,造一个好的轮子,也要花不少的时间。今天打算造一个 Mac OS 的菜单,实现 Mac 上的菜单功能,发现事情远没那么简单,没有很好的一个思路。
随即想去看看 ant-design 的 Trigger 的实现方式,结果就受到了打击。
我原有的一个思路是,实现一个 Trigger ,对于 React 来说,最主要的就是使用 React.createPortal 这个方法了,因为 Trigger 基本上都是根据子类元素的位置去确定弹出的内容的位置,所以必然是要插入一个 position 为 absolute 的元素,然后根据子元素的位置,去计算弹出的位置。
// 以下皆伪代码,几个原思路的核心功能,源代码被我一怒之下删了,冲动是魔鬼啊。// 要实现的组件类似这样<Trigger type="click" positon="bottom" popContent={<div>this is pop</div>}> <button>Open</button></Trigger>// 1. 插入弹窗,dom 是真实的 document 元素,popContent 是传入需要渲染的 react 节点,即 <div>this is pop</div> render() { return <div> {props.children} {React.createPortal(popContent, dom);} </div>}// 2. 创建 domconst dom = document.createElement('div');dom.style.position = "display";// 3. 计算 popContent 的位置// 利用 ref 得到 children 元素的位置,然后根据 offset 得到左和上的距离,再加上元素的宽高即可计算出 popContent 的 top 和 left 是多少// 4. 点击屏幕非 popContent 的空白处,关闭 popContent// 监听点击事件,通过判断点击位置是否在 popContent 内,而决定是否需要关闭
理想状态就是这样的,看起来一切都可行,
然而。。。
还是我太天真了。
到了第四步,在 useEfect 中添加一个全局的监听事件,肯定需要清除这个事件,然而清除之后又导致下一次触发 useEfect 的时候,又无法重新监听新的 ref 。后来实在没有好的想法了,就想着去看看别人的实现方式,首先就想到了 popover,经过一系列的排查,最终发现底层使用的是 rc-trigger, 于是就看 rc-trigger 的实现方式,看了一会,发现自己耐不住心去看,一大串一大串的代码,绕来绕去,看的头疼。
于是这就造就了我今天迷茫的一天,还想着有一天弄个 ant-design 源码解析系列呢,但如果这点代码都看不下去,以后怎么办?
(完)
]]>一篇关于 Git 常用命令的记录文章。
Git 在工作中成了必不可少的工具,个人比较喜欢使用 Git 命令,但是 Git 命令其实比较繁杂,有点难记,例如 rebase
, reset
等,只要不用的时间久了,就会忘记。所以特此记录。
用法: git clone [<options>] [--] <repo> [<dir>]
作用: 克隆一个仓库到本地
常用:
git clone -b test git@github.com:kavience/blog.git
用法: git status [<options>] [--] <pathspec>...
作用: 查看暂存区
常用:
用法: git add [<options>] [--] <pathspec>...
作用: 添加修改(包括添加、删除、修改等操作)到暂存区
常用:
git add .
用法: git log [<options>] [<revision-range>] [[--] <path>...]
作用: 查看日志
常用:
git log --author="username" --pretty=tformat: --numstat | awk '{ add += $1; subs += $2; loc += $1 - $2 } END { printf "added lines: %s, removed lines: %s, total lines: %s\n", add, subs, loc }' -
git log --format='%aN' | sort -u | while read name; do echo -en "$name\t"; git log --author="$name" --pretty=tformat: --numstat | awk '{ add += $1; subs += $2; loc += $1 - $2 } END { printf "added lines: %s, removed lines: %s, total lines: %s\n", add, subs, loc }' -; done
用法: git commit [<options>] [--] <pathspec>...
作用: 提交暂存区里面的修改,并生成一个唯一的 commit 号
常用:
用法: git push [<options>] [<repository> [<refspec>...]]
作用: 上传当前仓库的 commit 到远程仓库地址
常用:
git push --set-upstream [origin] [branch]
慎用!!!
)git push -f [origin] [branch]
git push [origin] [tag name]
git push [origin] --tags
上传所有 tag用法:
git fetch [<options>] [<repository> [<refspec>...]] or: git fetch [<options>] <group> or: git fetch --multiple [<options>] [(<repository> | <group>)...] or: git fetch --all [<options>]
作用: 拉取远程分支
常用:
git fetch [origin] [branch]
git fetch --all
用法:
git merge [<options>] [<commit>...] or: git merge --abort or: git merge --continue
作用: 合并分支
常用:
git merge [target_branch]
git merge --continue
用法: git pull [<options>] [<repository> [<refspec>...]]
作用: 拉取远程分支并与本地分支合并
常用:
git pull origin [origin_branch]:[local_branch]
git pull origin [branch]
,建立跟踪关系可以使用 git branch --set-upstream [local_branch] origin/[origin_branch]
用法:
git branch [<options>] [-r | -a] [--merged | --no-merged] or: git branch [<options>] [-l] [-f] <branch-name> [<start-point>] or: git branch [<options>] [-r] (-d | -D) <branch-name>... or: git branch [<options>] (-m | -M) [<old-branch>] <new-branch> or: git branch [<options>] (-c | -C) [<old-branch>] <new-branch> or: git branch [<options>] [-r | -a] [--points-at] or: git branch [<options>] [-r | -a] [--format]
作用: 查看、新建、删除、跟踪等操作分支
常用:
git branch
git branch -v
带最后一次 commitgit branch [branch_name]
git branch -d [branch]
git branch -D [branch]
强制删除,丢弃修改(慎用!!!)git branch --set-upstream [local_branch] origin/[origin_branch]
用法:
git checkout [<options>] <branch> or: git checkout [<options>] [<branch>] -- <file>...
作用: 切换分支、 tag 、 commit ,或创建且切换到新分支
常用:
git checkout [branch|commit|tag]
git checkout -b [branch]
用法:
git stash list [<options>] or: git stash show [<options>] [<stash>] or: git stash drop [-q|--quiet] [<stash>] or: git stash ( pop | apply ) [--index] [-q|--quiet] [<stash>] or: git stash branch <branchname> [<stash>] or: git stash clear or: git stash [push [-p|--patch] [-k|--[no-]keep-index] [-q|--quiet] [-u|--include-untracked] [-a|--all] [-m|--message <message>] [--] [<pathspec>...]] or: git stash save [-p|--patch] [-k|--[no-]keep-index] [-q|--quiet] [-u|--include-untracked] [-a|--all] [<message>]
作用: 暂存和恢复进度
常用:
git stash list
git stash
git stash apply
恢复最近的一次进度git stash apply [stash]
恢复指定的进度git stash drop [stash]
删除该进度git stash pop [stash]
恢复指定的进度并删除该进度用法: git commit [<options>] [--] <pathspec>...
作用: 合并多次 commit 、分支合并、保持一个简洁的 commit 信息
常用:
git rebase -i HEAD~[n]
n 为 commit 次数git rebase [branch]
合并分支到当前分支,和 merge 不一样的是不会产生 commit 信息,确保当前分支是只有本人使用,否则可能会产生丢失别人的 commit 信息。用法:
git reset [--mixed | --soft | --hard | --merge | --keep] [-q] [<commit>] or: git reset [-q] [<tree-ish>] [--] <paths>... or: git reset --patch [<tree-ish>] [--] [<paths>...] -q, --quiet be quiet, only report errors --mixed reset HEAD and index --soft reset only HEAD --hard reset HEAD, index and working tree --merge reset HEAD, index and working tree --keep reset HEAD but keep local changes --recurse-submodules[=<reset>] control recursive updating of submodules -p, --patch select hunks interactively -N, --intent-to-add record only the fact that removed paths will be added later
作用: 回滚到某个 commit
常用:
git reset --[soft|hard] HEAD
最近一次,等同于 git reset --[soft|hard] HEAD~0
git reset --[soft|hard] HEAD^
上一次,等同于 git reset --[soft|hard] HEAD~1
git reset --[soft|hard] HEAD^^
上两次,等同于 git reset --[soft|hard] HEAD~2
git reset --[soft|hard] HEAD^n^
上n次,等同于 git reset --[soft|hard] HEAD~n
用法:
git rm [<options>] [--] <file>... -n, --dry-run dry run -q, --quiet do not list removed files --cached only remove from the index -f, --force override the up-to-date check -r allow recursive removal --ignore-unmatch exit with a zero status even if nothing matched
作用:删除缓存文件
常用:
git rm -r --cached <file>
这只是个人工作中总结常用的一些命令,并不全面,会持续更新。
(完)
]]>CI: Continuous Integration ( 持续集成 )
CD: Continuous Delivery ( 持续交付 ) / Continuous Deployment ( 持续部署 )
先看 Readhat 的解释:
CI/CD 是一种通过在应用开发阶段引入自动化来频繁向客户交付应用的方法。CI/CD 的核心概念是持续集成、持续交付和持续部署。作为一个面向开发和运营团队的解决方案,CI/CD 主要针对在集成新代码时所引发的问题(亦称:“集成地狱”)。具体而言,CI/CD 可让持续自动化和持续监控贯穿于应用的整个生命周期(从集成和测试阶段,到交付和部署)。这些关联的事务通常被统称为“CI/CD 管道”,由开发和运维团队以敏捷方式协同支持。
再看维基百科的解释:
在软件工程中,CI/CD或CICD通常指的是持续集成和持续交付或持续部署的组合实践。CI/CD通过在应用程序的构建、测试和部署中实施自动化,在开发和运营团队之间架起了桥梁。
以下是个人观点:
持续集成的概念,类似开发者在合并新的代码到主干分支的时候,系统自动执行构建并执行测试,并将结果通知到开发者。
持续交付的概念,在代码集成且验证通过后,自动将验证的代码放入存储库,持续交付的目标是可以拥有一个随时部署的代码版本。
持续部署的概念,作为持续交付的延伸,可以自动将代码发布到生产环境。
基于以上概念的描述,我举 masos-web 这个例子
现在 masos-web 有三个分支,分别是 master
, dev
, feat-test
。
基于 Git 工作流开发, master
作为稳定的主分支代码,保证可以随时部署到生产环境。 dev
分支作为开发分支,是 master
分支的延伸,与 master
分支不会且不应该存在冲突。 feat-test
是相关的开发功能分支,编写相应的代码, feat-test
与 dev
存在冲突是正常的,因为有多个功能分支同时基于 dev
分支开发,功能分支禁止直接合并到 master
分支。
feat-test
分支上做了部分修改,然后合并到 dev
分支,通过 review
后,再执行合并。dev
分支与 master
分支合并,触发自动构建、代码检查等。持续交付,可生产多个版本,保证项目有多个可用的版本,一旦新版本发生了不可预知的错误,可随时使用旧版本。
除自动发布外,还可随时手动选择不同的版本发布。
注册登录
到 Travis 官网登录,安装指引注册、登录、授权。如下图:
申请 GitHub token
申请路径为: GitHub > settings > Developer settings > Personal access tokens > Generate new token
申请后,会得到一个例如 ghp_mZuC0e0gGxxxxxxxxxxxxxxxxx 的一个 token。
加密 GitHub token
使用 travis 加密这个 token。步骤为:
travis encrypt GITHUB_TOKEN=ghp_mZuC0e0gGxxxxxxxxxxxxxxxxx --com
secure
编写 .travis.yml
在项目下新建一个文件 .travis.yml
, 内容如下:
# 项目为 node 开发language: node_js# node 版本node_js:- 14# 任务队列 jobs: # 安装依赖 install: - yarn install # 执行一下自定义的脚本 script: # 因为 conventional-changelog-cli 和 standard-version 不用写在 package.json ,而是采用全局安装的方式 - yarn global add conventional-changelog-cli standard-version - yarn build - yarn release - yarn changelog - cp CHANGELOG.md build/CHANGELOG.md - mv build/ /tmp/build # 只有 master 分支触发构建 branches: - master # 部署到 github pages deploy: provider: pages local_dir: /tmp/build skip_cleanup: true github_token: "$GITHUB_TOKEN" keep_history: true on: branch: master # 部署后发布 tags after_deploy: - git push --follow-tags origin masterenv: global: # <secure> 替换为上一步生成的 secure - secure: <secure>
最新消息,travis 不再为开源项目免费提供使用。换成 GitHub action 吧。
打算好好学习一下 nodejs,那就先从 nodejs 官方文档看起吧,这篇博客主要是记录一下 nodejs 下的 c++ addons 模块,看 nodejs 为什么要引入 c++ ,以及如何引入 c++ 代码。
nodejs 采用事件驱动、异步编程,为网络服务而设计。其实 Javascript 的匿名函数和闭包特性非常适合事件驱动、异步编程。非阻塞模式的 IO 处理给 Node.js 带来在相对低系统资源耗用下的高性能与出众的负载能力,非常适合用作依赖其它 IO 资源的中间层服务。
Nodejs 虽然有着不错的异步能力,但是在密集型计算的时候却并不出众,简单来说所有的异步任务会维护在一个事件循环中(队列),线程会不断的去事件循环中取任务来执行,当 CPU 密集型的任务造成执行时间过长,就会导致其他任务无法执行,这样整个程序的性能就不行了。这个时候就可以用 c++ 来编写一些 nodejs 模块,加快运算时间。
以 fibonacci 函数为例,先看一下运行结果。
var fibonacciC = require("./build/Release/fibonacci.node").fibonacci;function fibonacciJS(n) { if (n <= 0) return 0; else if (n == 1) return 1; else return fibonacciJS(n - 1) + fibonacciJS(n - 2);}console.time("c++");console.log(fibonacciC(40));console.timeEnd("c++");console.time("js");console.log(fibonacciJS(40));console.timeEnd("js");// 输出102334155c++: 524.515ms102334155js: 1.335s
在 40 次的递归运算中,可以看到采用原生 js 实现的方式,时间是 c++ 方式实现的两倍多,如果计算时间更长的话,原生 js 实现的效率更低。
接下来看一下是如何把 c++ 的代码结合到 nodejs 中。
// fibonacci.ccusing namespace std;namespace demo {using v8::FunctionCallbackInfo;using v8::Isolate;using v8::Local;using v8::Object;using v8::Number;using v8::Value;int fib(int n) { if (n <= 0) return 0; else if (n == 1) return 1; else return fib(n - 1) + fib(n - 2);}/* 通过 FunctionCallbackInfo<Value>& args 可以设置返回值 */void fibonacci(const FunctionCallbackInfo<Value>& args) { Isolate *isolate = args.GetIsolate(); // node v10 版本之前是这样获取 number 参数的 // args[0]->NumberValue() // v10 版本之后是这样获取的 // args[0].As<Number>()->Value() Local<Number> num = Number::New(isolate, fib(args[0].As<Number>()->Value())); // 设置函数调用的返回值 args.GetReturnValue().Set(num); return ;}void Initialize(Local<Object> exports) { // 指定 module 名字 NODE_SET_METHOD(exports, "fibonacci", fibonacci);}// 加载 moduleNODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)}
# 首先要安装 node-gypnpm i -g node-gyp
在当前目录下添加 binding.gyp
文件,内容为:
{ "targets": [ { "target_name": "fibonacci", "sources": [ "fibonacci.cc" ] } ]}
运行打包编译命令
# 先打包node-gyp configure# 会生成一个 build 目录,然后进入这个目录cd build# 编译文件make
这个时候会生成一个 build 目录,以及 build 目录下的 Release 目录。
接下来就可以像这样引入该模块
var fibonacciC = require("./build/Release/fibonacci.node").fibonacci;console.log(fibonacciC(40));
引入 c++ ,借助其高效的计算能力,可以让 nodejs 也进行一些密集型的计算,本文只是一个简单的实例,更多强大的功能还需要看官方文档以及熟悉 c++ 才行。
]]>新版的 Apple M1 是基于 arm 架构的,然而大部分的软件 和 npm 包都还没有适配 arm 架构,所以苹果公司就使用 Rosetta 来兼容 x86 架构的软件。
第一次使用 x86 软件的时候,系统会自动提示安装 Rosetta ,安装后直接运行软件即可。
然而,今天遇到一个非常奇怪的问题,在安装依赖的时候我发现无法安装 node-canvas
这个包,经过一系列的排查,我发现问题出在这里:
...npm ERR! node-pre-gyp http 404 status code downloading tarball https://github.com/Automattic/node-canvas/releases/download/v2.7.0/canvas-v2.7.0-node-v88-darwin-unknown-arm64.tar.gz...
无法找到 canvas-v2.7.0-node-v88-darwin-unknown-arm64.tar.gz
这个包,顺着 node-canvas 的 releases 发现,根本就没有发布这个包,只有 canvas-v2.7.0-node-v88-darwin-unknown-x64.tar.gz
,这个时候我突然意识到,当前是以 arm 架构运行的 terminal 和 npm,所以识别出来后,自动去寻找适合 arm 架构的包,然而并没有适配 arm 的包,所以就导致无法安装。
我顺着使用 x86 架构的软件的思路,我觉得既然软件可以移植到 M1 上,npm 肯定也是可以的,一定有某种方法,而且肯定与 Rosetta 有关。终于,想到办法了。
显示简介
,发现有个选项 使用Rosetta打开
简而言之就是安装依赖的时候,发现依赖不适用于 arm 架构的 M1,只能使用 x86 架构的依赖,所以需要借助于 Rosetta 去安装依赖。
]]>简单、可扩展的状态管理,相比于 redux,更轻巧,更简单,更灵活,在某些时候性能甚至更优越。
在这里简单的记录和介绍一下 mobx 的使用。
import { observable } from "mobx";import { observer } from "mobx-react";var numStore = observable({ num: 1, addNum: function () { this.num++; },});@observerclass TimerView extends React.Component { handleAdd = () => { this.props.numStore.addNum(); }; render() { return ( <div> <span> num: {this.props.numStore.num} </span> <button onClick={this.handleAdd}></button> </div> ); }}ReactDOM.render(<TimerView numStore={numStore} />, document.body);
使用:
Observable 值可以是 JS 基本数据类型、引用类型、普通对象、类实例、数组和映射。 主要作用是指定该值的是被观察的、可修改的。其装饰器写法为 @observable
。例如
import { observable, computed } from "mobx";// 方法1,直接使用var NumStore = observable({ num: 1, addNum: function () { this.num++; },});// 方法二,装饰器写法class NumStore { @observable price = 0; @observable num = 1; @computed get total() { return this.price * this.num; }}
如果任何影响计算值的值发生变化了,计算值将根据状态自动进行衍生。 计算值在大多数情况下可以被 MobX 优化的,因为它们被认为是纯函数。 例如,如果前一个计算中使用的数据没有更改,计算属性将不会重新运行。 如果某个其它计算属性或 reaction 未使用该计算属性,也不会重新运行。 在这种情况下,它将被暂停。
import { observable, computed } from "mobx";class NumStore { @observable price = 0; @observable num = 1; @computed get total() { return this.price * this.num; }}
autorun 可以用来监听值的变化,不要把 computed
和 autorun
搞混。它们都是响应式调用的表达式,但是,如果你想响应式的产生一个可以被其它 observer 使用的值,请使用 @computed,如果你不想产生一个新值,而想要达到一个效果,请使用 autorun。 举例来说,效果是像打印日志、发起网络请求等这样命令式的副作用。
import axios from "axios";import { observable, configure, action, runInAction, autorun } from "mobx";configure({ enforceActions: "observed" });export class Session { @observable num = 1; constructor() { autorun(() => { // 每次调用 addNum / subNum都会执行此函数 console.log("auto log num:" + this.num); }); } @action addNum = function () { console.log(this.num); this.num++; }; @action subNum = () => { this.num--; };}export default new Session();
用法:
action 主要是用来修改状态,也可以使用异步的方法
import axios from "axios";import { observable, configure, action, runInAction } from "mobx";// 强制使用 action 来修改状态,否则会打印 waringconfigure({ enforceActions: "observed" });export class Session { @observable num = 1; @observable loading = false; @action addNum = function () { console.log(this.num); this.num++; }; @action subNum = () => { this.num--; }; @action directGetHundred = () => { this.loading = true; setTimeout( // 所有的修改状态都需要放在 action 中 action("directAddHundred", () => { this.num += 100; this.loading = false; }), 1000 ); }; @action directGetTwoHundred = async () => { this.loading = true; await axios("/"); // 调用其他异步操作 // await axios("/"); // runInAction 是 action 的语法糖,鼓励你不要到处写 action,而是在整个过程结束时尽可能多地对所有状态进行修改 runInAction(() => { this.loading = false; this.num += 200; }); };}export default new Session();
flows 的工作原理与 async / await 是一样的。只是使用 function * 来代替 async,使用 yield 代替 await 。 使用 flow 的优点是它在语法上基本与 async / await 是相同的 (只是关键字不同),并且不需要手动用 @action 来包装异步代码,这样代码更简洁。
flow 只能作为函数使用,不能作为装饰器使用。 flow 可以很好的与 MobX 开发者工具集成,所以很容易追踪 async 函数的过程。
mobx.configure({ enforceActions: true });class Store { @observable githubProjects = []; @observable state = "pending"; fetchProjects = flow(function* () { // <- 注意*号,这是生成器函数! this.githubProjects = []; this.state = "pending"; try { const projects = yield fetchGithubProjectsSomehow(); // 用 yield 代替 await const filteredProjects = somePreprocessing(projects); // 异步代码块会被自动包装成动作并修改状态 this.state = "done"; this.githubProjects = filteredProjects; } catch (error) { this.state = "error"; } });}
之前一直使用 redux,看到了有赞前端技术团队的 我为什么从 Redux 迁移到了 Mobx
文章,决定了解一下 mobx ,现在这里只记录 mobx 的简单使用,详细的还是需要查看官方文档。
三者都是 TypeScript 的类型, never 是最具体的类型,因为没有哪个集合比空集合更小了;而 unknown 是最弱的类型,因为它包含了全部可能的值。 any 则不为集合,它破坏了类型检查,因此请尽量不要使用 any。在 TypeScript 中, nerver 可以赋值为 unknown 和 any ,但是 unknown 和 any 不可以赋值给 never,never 只能赋值 never。
那 nerver 的作用是什么呢?举个尤雨溪提到的例子:
interface Foo { type: "foo";}interface Bar { type: "bar";}type All = Foo | Bar;function handleValue(val: All) { switch (val.type) { case "foo": // 这里 val 被收窄为 Foo break; case "bar": // val 在这里是 Bar break; default: // val 在这里是 never const exhaustiveCheck: never = val; break; }}
注意在 default 里面我们把被收窄为 never 的 val 赋值给一个显式声明为 never 的变量。如果一切逻辑正确,那么这里应该能够编译通过。但是假如后来有一天你的同事改了 All 的类型:
type All = Foo | Bar | Baz;
然而他忘记了在 handleValue 里面加上针对 Baz 的处理逻辑,这个时候在 default branch 里面 val 会被收窄为 Baz,导致无法赋值给 never,产生一个编译错误。所以通过这个办法,你可以确保 handleValue 总是穷尽 (exhaust) 了所有 All 的可能类型。
// 使用 interfaceinterface User { name: string; age: number;}interface SetUser { (name: string, age: number): void;}
// 使用 typetype User = { name: string; age: number;};type SetUser = (name: string, age: number) => void;
interface Name { name: string;}interface User extends Name { age: number;}
type Name = { name: string;};type User = Name & { age: number };
type 可以声明基本类型别名,联合类型,元组等类型
// 基本类型别名type Name = string;// 联合类型interface Dog { wong();}interface Cat { miao();}type Pet = Dog | Cat;// 具体定义数组每个位置的类型type PetList = [Dog, Pet];
type 语句中还可以使用 typeof 获取实例的 类型进行赋值
// 当你想获取一个变量的类型时,使用 typeoflet div = document.createElement("div");type B = typeof div;
其它用法
type StringOrNumber = string | number;type Text = string | { text: string };type NameLookup = Dictionary<string, Person>;type Callback<T> = (data: T) => void;type Pair<T> = [T, T];type Coordinates = Pair<number>;type Tree<T> = T | { left: Tree<T>; right: Tree<T> };
interface 能够声明合并
interface User { name: string; age: number;}interface User { sex: string;}/*User 接口为 { name: string age: number sex: string }*/
]]>Number, String, Bool, Null, Undefined, Symbol, 引用数据类型( Object, Function, Array )。
Null 与 Undefined 的区别:
Null
作为函数的参数, 表示该函数的参数不是对象。
作为对象原型链的终点。
Undefined
变量被声明了, 但没有赋值时, 就等于 undefined。
调用函数时, 应该提供的参数没有提供, 该参数等于 undefined。
对象没有赋值的属性, 该属性的值为 undefined。
函数没有返回值时, 默认返回 undefined。
声明方式 | 变量提升 | 暂时性死区 | 重复声明 | 初始值 | 作用域 |
---|---|---|---|---|---|
var | 允许 | 不存在 | 允许 | 不需要 | 除块级 |
let | 不允许 | 存在 | 不允许 | 不需要 | 块级 |
const | 不允许 | 存在 | 不允许 | 需要 | 块级 |
function Person(name) { this.name = name;}Person.prototype = { constructor: Person, sayName: function() { alert(this.name);};var jack = new Person("Jack");
首先, 必须保证 new 运算符后跟着的是一个有[[Construct]]
内部方法的对象, 否则会抛出异常。
接下来就是创建对象的过程:
先创建一个原生对象, 假定为 obj = {} 或 obj = new Object。
获得构造函数 Person 的 prototype 对象, 并将其赋给 obj 的[[Prototype]]
属性, 表现为__proto__
。
call 构造函数的内部方法, 把其中的 this 赋值为新创建的对象 obj, 并传入所需参数。
执行构造函数, 并返回创建的对象。
这里有一点需要说明: 正常来讲构造函数中是不用写 return 语句的, 因为它会默认返回新创建的对象。但是, 如果在构造函数中写了 return 语句, 如果 return 的是一个对象, 那么函数就会覆盖掉新创建的对象, 而返回此对象;如果 return 的是基本类型如字符串、数字、布尔值等, 那么函数会忽略掉 return 语句, 还是返回新创建的对象。
function myNew(Fn) { const obj = {}; if (typeof Fn === "function") { obj.__proto__ = Fn.prototype; const args = Array.from(arguments).slice(1); Fn.call(obj, ...args); } return obj;}function Person(name) { this.name = name;}const person = myNew(Person, "张三");console.log(person.name);
在 js 中 this 一般会出现在如下情况:
详见另一篇文章 - js-彻底了解 this 的指向
bind 与 call 或 apply 最大的区别就是 bind 不会被立即调用, 而是返回一个函数, 函数内部的 this 指向与 bind 执行时的第一个参数, 而传入 bind 的第二个及以后的参数作为原函数的参数来调用原函数。
call, apply 都是为了改变某个函数运行时的上下文而存在的, 简单点说就是为了改变某个运行时函数内部 this 指向, 区别在于 apply 第二参数需要是一个参数数组, call 的第二参数及其之后的参数需要是数组里面的元素。
Function.prototype.apply = function (context, arr) { // 基础类型转包装对象 if (context === undefined || context === null) { context = window; } else if (typeof context === "string") { context = new String(context); } else if (typeof context === "number") { context = new Number(context); } else if (typeof context === "boolean") { context = new Boolean(context); } // 非对象, 非undefined, 非null的值才会抛错 if ( typeof arr !== "object" && typeof arr !== "undefined" && typeof arr !== "null" ) throw new TypeError("CreateListFromArrayLike called on non-object"); arr = (Array.isArray(arr) && arr) || []; // 非数组就赋值空数组 // 保存原函数至指定对象的fn属性上 context.fn = this; // 通过指定对象的fn属性执行原函数并出入参数 const fnValue = context.fn(...arr); delete context.fn; // 从context中删除fn原函数 return fnValue;};
Function.prototype.call = function (context) { // 基础类型转包装对象 if (context === undefined || context === null) { context = window; } else if (typeof context === "string") { context = new String(context); } else if (typeof context === "number") { context = new Number(context); } else if (typeof context === "boolean") { context = new Boolean(context); } // 保存原函数至指定对象的fn属性上 context.fn = this; // 获取除第一个参数之后的所有参数 const args = Array.from(arguments).slice(1); // 通过指定对象的fn属性执行原函数并出入参数 const fnValue = context.fn(...args); delete context.fn; // 从context中删除fn原函数 return fnValue;};
Function.prototype.bind = function (context) { // 保存原函数 const ofn = this; // 获取除第一个参数之后的所有参数 const args = Array.from(arguments).slice(1); function O() {} function fn() { // 第一个参数的判断是为了忽略使用new实例化函数时让this指向它自己, 否则就指向这个context指定对象 // 第二个参数的处理做了参数合并, 就是 bind & fn 两个函数的参数合并 ofn.apply( this instanceof O ? this : context, args.concat(Array.from(arguments)) ); } O.prototype = this.prototype; fn.prototype = new O(); return fn;};
概要: 每个构造函数( construct ) 都有一个原型对象, 原型对象( prototype )都包含一个指向构造函数的内部指针, 而实例( instance ) 都包含指向原型对象的内部指针。实例与原型的链条称作
原型链
。
网上看到一张图, 感觉很全面的描述了原型链之间的关系:
注意: prototype
是函数(ES6 中箭头函数除外)特有的属性, 实例对象不存在该属性, __proto__
则在两者内都存在, 因为函数也是对象。
七种 JS 继承方式分别是:
基本思想: 通过直接改变子类的 prototype 实现。
优点: 实例可继承的属性有: 实例的构造函数的属性, 父类构造函数的属性, 父类原型上的属性(新实例不会继承父类实例的属性)。
缺点: 新实例无法向父类构造函数传参, 继承单一, 所有新实例都会共享父类实例的属性。
function Person(name) { this.name = name;}Person.prototype.job = "frontend";function Child() { this.name = "child";}Child.prototype = new Person();var child = new Child();console.log(child.job); // frontendconsole.log(child instanceof Person); // true
基本思想: 在子类型构造函数的内部调用超类型构造函数.
优点: 保证了原型链中引用类型值的独立, 不再被所有实例共享, 子类型创建时也能够向父类型传递参数。
缺点: 方法都在构造函数中定义, 函数难以复用, 而且父类中定义的方法, 对子类而言也是不可见的。
function Father() { this.colors = ["red", "blue", "green"];}function Son() { Father.call(this); //继承了Father,且向父类型传递参数}var instance1 = new Son();instance1.colors.push("black");console.log(instance1.colors); //"red,blue,green,black"var instance2 = new Son();console.log(instance2.colors); //"red,blue,green" 可见引用类型值是独立的
基本思路: 使用原型链实现对原型属性和方法的继承, 通过借用构造函数来实现对实例属性的继承。
优点: 通过在原型上定义方法实现了函数复用, 又能保证每个实例都有它自己的属性。
缺点: 调用了两次父类构造函数, 造成了不必要的消耗。
function Father(name) { this.name = name; this.colors = ["red", "blue", "green"];}Father.prototype.sayName = function () { alert(this.name);};function Son(name, age) { Father.call(this, name); //继承实例属性, 第一次调用Father() this.age = age;}Son.prototype = new Father(); //继承父类方法,第二次调用Father()Son.prototype.sayAge = function () { alert(this.age);};var instance1 = new Son("louis", 5);instance1.colors.push("black");console.log(instance1.colors); //"red,blue,green,black"instance1.sayName(); //louisinstance1.sayAge(); //5var instance1 = new Son("zhai", 10);console.log(instance1.colors); //"red,blue,green"instance1.sayName(); //zhaiinstance1.sayAge(); //10
基本思想: 也是通过 prototype 完成继承, 只不过在多了一层函数调用。
优点: 用一个函数包装一个对象, 然后返回这个函数的调用, 这个函数就变成了可以随意增添属性的实例或对象。Object.create()
就是这个原理。
缺点: 所有的实例都会继承原型上的属性, 无法实现复用。
// 先封装一个函数容器, 用来承载继承的原型和输出对象function object(obj) { function F() {} F.prototype = obj; return new F();}function Person(name) { this.name = name;}var super0 = new Person();var super1 = object(super0);console.log(super1 instanceof Person); // true
基本思想: 寄生式继承的思路与(寄生)构造函数和工厂模式类似, 即创建一个仅用于封装继承过程的函数, 该函数在内部以某种方式来增强对象, 最后再像真的是它做了所有工作一样返回对象。
优点: 借助原型可以基于已有的对象创建新对象, 同时还不必因此创建自定义类型。
缺点: 使用寄生式继承来为对象添加函数, 会由于不能做到函数复用而降低效率, 这一点与构造函数模式类似。
function object(obj) { // 通过 prototype 继承 function F() {} F.prototype = obj; return new F();}function Person(name) { this.name = name;}var sup = new Person();function subobject(obj) { var sub = object(obj); sub.name = "ming"; return sub;}var sup2 = subobject(sup);// 这个函数经过声明后就成了可增添属性的对象console.log(sup2.name); // 'ming'console.log(sup2 instanceof Person); // true
基本思想: 不必为了指定子类型的原型而调用超类型的构造函数。
优点: 集寄生式继承和组合继承的优点于一身, 是实现基于类型继承的最有效方法。
function Person(name) { this.name = name;}// 寄生function object(obj) { function F() {} F.prototype = obj; return new F();}// object是F实例的另一种表示方法var obj = object(Person.prototype);// obj实例(F实例)的原型继承了父类函数的原型// 上述更像是原型链继承, 只不过只继承了原型属性// 组合function Sub() { this.age = 100; Person.call(this); // 这个继承了父类构造函数的属性} // 解决了组合式两次调用构造函数属性的特点Sub.prototype = obj;console.log(Sub.prototype.constructor); // Personobj.constructor = Sub; // 重点, 一定要修复实例console.log(Sub.prototype.constructor); // Subvar sub1 = new Sub();// Sub实例就继承了构造函数属性, 父类实例, object的函数属性console.log(sub1.job); // frontendconsole.log(sub1 instanceof Person); // true
ES6 关键字 extends 继承本质也是组合式继承。
]]>浏览器的垃圾回收机制(Garbage collection
), 简称 GC, 它会周期性运行以释放那些不需要的内存, 否则, JavaScript 的解释器将会耗尽全部系统内存而导致系统崩溃。具体到浏览器中的实现, 通常有两个策略: 标记清除和引用计数。
此算法把“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。如果没有引用指向该对象(零引用), 对象将被垃圾回收机制回收。引用计数法是最初级的垃圾收集算法, 如果某对象没有其他对象指向它了, 那就说明它可以被回收。但是它无法处理循环引用的问题。我们执行 f 函数, 它返回了一个数字, 和内部的 o1,o2 没什么关系, 但是对引用计数法来说, o1,o2 它们之间还存在着相互引用, 并不会被回收。这就造成了内存泄漏。
这个算法把“对象是否不再需要”简化定义为“对象是否可以获得”。
从 2012 年起, 所有现代浏览器都使用了标记-清除垃圾回收算法。标记清除法假定存在一个根对象(相当于 js 的全局对象), 垃圾回收器将定期从根对象开始查找, 凡是从根部出发能扫描到的都会保留, 扫描不到的将被回收。
内部流程
全局变量
全局变量什么时候需要自动释放内存空间很难判断, 所以在开发中尽量避免使用全局变量, 以提高内存有效使用率。
未移除的事件绑定
dom 元素虽然被移除了, 但元素绑定的事件还在, 如果不及时移除事件绑定, 在 IE9 以下版本容易导致内存泄漏。现代浏览器不存在这个问题了, 了解一下即可
定时器 setInterval/setTimeout
看下面的一段定时器代码, 一旦我们在其它地方移除了 node 节点, 定时器的回调便失去了意义, 然而它一直在执行导致 callback 无法回收, 进而造成 callback 内部掉数据 resData 也无法被回收。所以我们应该及时 clear 定时器。
let resData = 100;let callback = function () { let node = document.querySelecter(".p"); node && (node.innerHTML = resData);};setInterval(callback, 1000);
es6 的 WeakMap 和 Map 类似, 都是用于生成键值对的集合, 不同的是 WeakMap 是一种弱引用, 它的键名所指向的对象, 不计入垃圾回收机制, 另外就是 WeakMap 只接受对象作为键名(null 除外), 而 Map 可以接受各种类型的数据作为键。
WeakMap 这种结构有助于防止内存泄漏, 一旦消除对键的引用, 它占用的内存就会被垃圾回收机制释放。WeakMap 保存的这个键值对, 也会自动消失。包括 WeakSet 也是类似的, 内部存储的都是弱引用对象, 不会被计入垃圾回收。
看一个阮一峰 ES6 文档上举的例子:
let myWeakmap = new WeakMap();myWeakmap.set(document.getElementById("logo"), { timesClicked: 0 });document.getElementById("logo").addEventListener( "click", function () { let logoData = myWeakmap.get(document.getElementById("logo")); logoData.timesClicked++; }, false);
上面代码中, 我们将 dom 对象作为键名, 每次点击, 我们就更新一下状态。我们将这个状态作为键值放在 WeakMap 里。一旦这个 DOM 节点删除, 该状态就会自动消失, 不存在内存泄漏风险。
WeakSet 和 WeakMap 类似, 它和 set 结构的区别也是两点:
WeakSet 中的对象都是弱引用, 不会被计入垃圾回收
成员只能是对象, 而不能是其他类型的值
所以从垃圾回收的角度来看, 合理的使用 WeakMap 和 WeakSet, 能帮助我们避免内存泄漏。
告诉浏览器使用哪个版本的 HTML 规范来渲染文档。DOCTYPE 不存在或形式不正确会导致 HTML 文档以混杂模式呈现。
标准模式(Standards mode)以浏览器支持的最高标准运行;混杂模式(Quirks mode)中页面是一种比较宽松的向后兼容的方式显示。
行内元素: a span img input select
块级元素: div h1 p ul ol li dl dt dd
空元素:
相同的地方, 都是外部引用 CSS 方式, 区别:
link 是 xhtml 标签, 除了加载 css 外, 还可以定义 RSS 等其他事务;@import 属于 CSS 范畴, 只能加载 CSS
link 引用 CSS 时候, 页面载入时同时加载;@import 需要在页面完全加载以后加载, 而且@import 被引用的 CSS 会等到引用它的 CSS 文件被加载完才加载
link 是 xhtml 标签, 无兼容问题;@import 是在 css2.1 提出来的, 低版本的浏览器不支持
link 支持使用 javascript 控制去改变样式, 而@import 不支持
link 方式的样式的权重高于@import 的权重
import 在 html 使用时候需要<style type="text/css">
标签
主要分成两部分: 渲染引擎(Layout Engine 或 Rendering Engine)和 JS 引擎。
渲染引擎: 负责取得网页的内容(HTML、XML、图像等等)、整理讯息(例如加入 CSS 等), 以及计算网页的显示方式, 然后会输出至显示器或打印机。浏览器的内核的不同对于网页的语法解释会有不同, 所以渲染的效果也不相同。
JS 引擎: 解析和执行 javascript 来实现网页的动态效果。
最开始渲染引擎和 JS 引擎并没有区分的很明确, 后来 JS 引擎越来越独立, 内核就倾向于只指渲染引擎。
Trident( MSHTML ): IE MaxThon TT The World 360 搜狗浏览器
Geckos: Netscape6 及以上版本 FireFox Mozilla Suite/SeaMonkey
Presto: Opera7 及以上(Opera 内核原为: Presto, 现为: Blink)
Webkit: Safari Chrome
新增加了图像、位置、存储、多任务等功能。
新增元素:
移除的元素:
区分:
DOCTYPE 声明的方式是区分重要因素
根据新增加的结构、功能来区分
共同点: 都是保存在浏览器端, 且是同源的。
区别:
优点:
缺点:
HTML 的输入框可以拥有自动完成的功能, 当你往输入框输入内容的时候, 浏览器会从你以前的同名输入框的历史记录中查找出类似的内容并列在输入框下面, 这样就不用全部输入进去了, 直接选择列表中的项目就可以了。但有时候我们希望关闭输入框的自动完成功能, 例如当用户输入内容的时候, 我们希望使用 AJAX 技术从数据库搜索并列举而不是在用户的历史记录中搜索。
方法:
在 alt 和 title 同时设置的时候, alt 作为图片的替代文字出现, title 是图片的解释文字。
盒模型分为标准盒模型与怪异盒模型( 也称为 IE 盒模型 )
标准盒模型下 width 和 height 为内容的宽高, 怪异盒模型下 width 和 height 为内容的宽高加上 border 的宽高, 再加上 padding 的宽高。
.box { /* Chrome 默认标准盒模型 */ box-sizing: "content-box"; /* 标准盒模型 */}.box { box-sizing: "border-box"; /* 怪异盒模型 */}
大部分的开源组件库, 例如 ant-design 都使用怪异盒模型, 不会造成布局破坏。
通过样式处理
.parent { position: relative; width: 500px; height: 500px;}.child { width: 100px; height: 100px; background: red;}/* 方法一, 子级知道宽高 */.child { position: absolute; width: 200px; height: 200px; top: 50%; left: 50%; margin-left: -100px; margin-top: -100px; background: red;}/* 方法二, 使用 margin: auto */.child { position: absolute; width: 200px; height: 200px; top: 0; left: 0; bottom: 0; right: 0; margin: auto; background: red;}/* 方法三, 子级不知道宽高, 使用 transform */.child { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: red;}/* 方法四, 使用 flex 布局 */.parent { position: relative; display: flex; justify-content: center; align-items: center; width: 500px; height: 500px;}/* 方法六, 利用 table-cell 布局 */.parent { display: table-cell; vertical-align: middle; text-align: center;}
通过 js 处理样式, 本质与 css 处理一致
// 方法五 jslet parentWidth = document.getElementById("parent").clientWidth, parentHeight = document.getElementById("parent").clientHeight, childWidth = document.getElementById("child").clientWidth, childHeight = document.getElementById("child").clientHeight;document.getElementById("parent").style.position = "relative";document.getElementById("child").style.position = "absolute";console.log((parentWidth - childWidth) / 2);document.getElementById("child").style.left = (parentWidth - childWidth) / 2 + "px";document.getElementById("child").style.top = (parentHeight - childHeight) / 2 + "px";
px 像素。绝对单位, 像素 px 是相对于显示器屏幕分辨率而言的, 是一个虚拟单位。是计算机系统的数字化图像长度单位, 如果 px 要换算成物理长度, 需要指定精度 DPI。
em 是相对长度单位, 相对于当前对象内文本的字体尺寸。如当前对行内文本的字体尺寸未被人为设置, 则相对浏览器的默认字体尺寸。它会继承父级元素的字体大小, 因此并不是一个固定的值。
rem 是 CSS3 新增的一个相对单位(root em,根 em),使用 rem 为元素设定字体大小事, 仍然是相对大小但相对的只是 HTML 根元素。
区别: IE 无法调用那些使用 px 作为单位的字体大小, 而 em 和 rem 可以缩放, rem 相对的只是 HTML 根元素。这个单位可谓集相对大小和绝对大小的优点于一身, 通过它既可以做到只修改根元素就成比例地调整所有字体大小, 又可以避免字体大小逐层复合的连锁反应。目前, 除了 IE8 及更早版本外, 所有浏览器已支持 rem。
::selection
display:none;
使用该属性后, HTML 元素(对象)的宽高, 高度等各种属性值都将“丢失”;visibility:hidden;
使用该属性后, HTML 元素(对象)仅仅是在视觉上看不见(完全透明), 而它所占据的空间位置仍然存在, 也即是说它仍然具有高度, 宽度等属性值。
css 选择符有: 类选择器、标签选择器、ID 选择器、后代选择器(派生选择器)、群组选择器
:link、:visited、:hover、:active 按照 LVHA(LoVe HAte)顺序定义
等级 | 标签内选择符 | ID 选择符 | Class 选择符/属性选择符/伪类选择符 | 元素选择符 |
---|---|---|---|---|
示例 | <span style="color:red;"> | #text{color:red;} | .text{color:red;} [type=“text”]{color:red} | span{color:red;} |
标记位 | x,0,0,0 | 0,x,0,0 | 0,0,x,0 | 0,0,0,x |
特点:
!important 优先级最高。
BFC(Block formatting context)直译为"块级格式化上下文"。它是一个独立的渲染区域, 只有 Block-level box 参与, 它规定了内部的 Block-level Box 如何布局,
并且与这个区域外部毫不相干。
BFC 布局规则:
.div { pointer-envets: none;}
]]>基于 react-dnd
和 react-dnd-html5-backend
创建拖拽节点与背景画布。
基于 g6
创建可视化图,根据 api 提供的 registerBehavior
注册行为,监听鼠标事件,基于 g6 提供的 ToolBar
, Menu
, Minimap
, Grid
等插件,提供更多功能。
基于 antd
提供的 UI 组件优化样式。
请查看 在线 Demo
项目仅供学习和参考,或许不适合直接用于项目中,项目地址;
]]>对 js 的 this 指向问题还是会有点模糊,我决定下点功夫,写下这篇文章,彻底把 this 搞明白。
这也是我发出的第一个问题,究竟什么是 this ?在 js 中 this 代表的到底是什么?根据 w3c 的描述:
The JavaScript this keyword refers to the object it belongs to.
在 js 中 this 关键字代表它所属对象的引用。
再根据 MDN 的描述,
In the global execution context (outside of any function), this refers to the global object whether in strict mode or not.
this 表示当前执行上下文( global、function 或 eval )的一个属性,在非严格模式下,总是指向一个对象,在严格模式下可以是任意值。
由此可见 this 的指向是不确定的,是在运行时确定的,而且 this 在不同的情况下,其代表的含义也不一样。
下面我将通过本文,彻底分析 this 的所有形式。
注:全局对象,在浏览器端代表 window 对象,在 nodejs 环境下代表 global 对象,以下不再区分,简称全局对象。
首先要考虑 this 一般会出现在哪些情况呢?
其实 this 一般都是出现在函数内,所以在第六点单独称之为 「一般函数」,下面分别分析。
无论是否在严格模式下,在全局执行环境中(在任何函数体外部) this 都指向全局对象
。如:
console.log(this); // 全局对象
在全局状态下、对象内、class 内等的函数,我称之为一般函数,也是使用最多的情况。
在普通函数内部,this 的值取决于函数被调用的方式。
在严格模式下,如果进入执行环境时没有设置 this 的值,this 会保持为 undefined,非严格模式下指向全局对象。
在箭头函数中,this 取决于函数被创建时的环境。
因此在全局情况下,无论是否为严格模式,this 指向都是全局对象。
请务必记住以上重点标记的两句话,在很多地方也都用到。
function f2() { ; console.log(this);}function f3() { console.log(this);}const f4 = () => { console.log(this);};const f5 = () => { ; console.log(this);};f2(); // undefinedf3(); // 全局对象f4(); // 全局对象f5(); // 全局对象
const obj = { type: 1, func1: function () { console.log(this); }, func2: () => { console.log(this); },};obj.func1(); // objobj.func2(); // 全局对象// 把 func1,func2 单独拿出来调用,在当前情况下等同于全局状态下的调用const { func1, func2 } = obj;func1(); // 全局对象func2(); // 全局对象
在严格模式下再运行一遍
const obj = { type: 1, func1: function () { console.log(this); }, func2: () => { console.log(this); },};obj.func1(); // objobj.func2(); // 全局对象// 把 func1,func2 单独拿出来调用,在当前情况下等同于全局状态下的调用const { func1, func2 } = obj;func1(); // undefinedfunc2(); // 全局对象
;
当作为对象的函数时,this 的绑定只受最接近的成员引用的影响。
function independent() { return this.prop;}const independent2 = () => { return this.prop;};var obj = { prop: 37, func1: function () { return this.prop; }, func2: () => { return this.prop; },};obj.func3 = independent;obj.func4 = independent2;obj.child = { func5: independent, func6: independent2, prop: 42 };console.log(obj.func1()); // 37console.log(obj.func2()); // undefinedconsole.log(obj.func3()); // 37console.log(obj.func4()); // undefinedconsole.log(obj.child.func5()); // 42console.log(obj.child.func6()); // undefined
this 在类中的表现与在函数中类似,因为类本质上也是函数,但也有一些区别和注意事项。在类的构造函数中,this 是一个常规对象。类中所有非静态的方法都会被添加到 this 的原型中
。
和其他普通函数一样,类方法中的 this 值取决于它们如何被调用。需要注意的是类内部总是严格模式
。类的方法内部如果含有 this,它默认指向类的实例。但是,必须非常小心,一旦单独使用该方法,很可能报错。
class TestClass { normalFunction() { console.log("normal function:", this); } arrowFunction = () => { console.log("arrow function:", this); };}const t = new TestClass();const { normalFunction, arrowFunction } = t;normalFunction(); // normal function: undefinedarrowFunction(); // arrow function: TestClass {arrowFunction: ƒ}
在上面代码中
const { normalFunction, arrowFunction } = t;
其实相当于如下代码
function normalFunction2() { ; console.log("normal function:", this);}
因为普通函数的 this 是由调用者确定的,如果在非严格模式下,直接调用,则 this 指向全局对象,如果是严格模式下,this 则为 undefined。而箭头函数是由创建时就确定了,所以 arrowFunction 实际指向的仍是 TestClass 实例。
有时,也可以通过 bind 方法使类中的 this 值总是指向这个类实例。为了做到这一点,可在构造函数中绑定类方法:
class Car { constructor() { // 注意 bind 和无 bind 的区别 this.sayBye = this.sayBye.bind(this); } sayHi() { console.log(`Hello from ${this.name}`); } sayBye() { console.log(`Bye from ${this.name}`); } get name() { return "Ferrari"; }}class Bird { get name() { return "Tweety"; }}const car = new Car();const bird = new Bird();// class 中方法的调用取决于调用者car.sayHi(); // Hello from Ferraribird.sayHi = car.sayHi;bird.sayHi(); // Hello from Tweety// 对于已绑定的函数,this 就不在依赖调用者bird.sayBye = car.sayBye;bird.sayBye(); // Bye from Ferrari
在派生类中的构造函数没有初始的 this 绑定。在构造函数中调用 super()
会生成一个 this 绑定。所以在子类的构造函数中,如果要使用 this 的话必须要调用 super()
,相当于 this = new Base();
。派生类不能在调用 super() 之前返回,除非其构造函数返回的是一个对象,或者根本没有构造函数。
class Base {}class Good extends Base {}class AlsoGood extends Base { constructor() { return { a: 5 }; }}class Bad extends Base { constructor() {}}new Good();new AlsoGood();new Bad(); // ReferenceError
更多关于 class 的内容可以查看阮一峰老师关于 class 的说明
通过函数的 call, apply, bind 方法是可以改变 this 的指向的,例如:
var obj = { a: "Custom" };var a = "Global";function func1() { return this.a;}// ECMAScript 5 引入了 Function.prototype.bind()。调用 f.bind(someObject)会创建一个与 f 具有相同函数体和作用域的函数,但是在这个新函数中,this 将永久地被绑定到了 bind 的第一个参数,无论这个函数是如何被调用的。const func2 = func1.bind(obj);func1(obj);func1.call(obj);func1.apply(obj);func2(obj);// 注意:如果将this传递给call、bind、或者apply来调用箭头函数,它将被忽略。不过你仍然可以为调用添加参数,不过第一个参数(thisArg)应该设置为null。
在非严格模式下使用 call 和 apply 时,如果用作 this 的值不是对象,则会被尝试转换为对象。null 和 undefined 被转换为全局对象。原始值如 7 或 ‘foo’ 会使用相应构造函数转换为对象。因此 7 会被转换为 new Number(7) 生成的对象,字符串 ‘foo’ 会转换为 new String(‘foo’) 生成的对象。
// 非严格模式下function bar() { // 此处也属于函数内的 this ,下面会继续分析 console.log(this);}bar.call(7); // Number {7}bar.call("foo"); // String {"foo"}bar.call(undefined); // 全局对象
// 严格模式下 ;function bar() { console.log(this);}bar.call(7); // 7bar.call("foo"); // foobar.call(undefined); // undefined
ECMAScript 5 引入了 Function.prototype.bind()
。调用 f.bind(someObject)会创建一个与 f 具有相同函数体和作用域的函数,但是在这个新函数中,this 将永久地被绑定到了 bind 的第一个参数,无论这个函数是如何被调用的。
function f() { return this.a;}var g = f.bind({ a: "azerty" });console.log(g()); // azertyvar h = g.bind({ a: "yoo" }); // bind只生效一次!console.log(h()); // azertyvar o = { a: 37, f: f, g: g, h: h };console.log(o.a, o.f(), o.g(), o.h()); // 37, 37, azerty, azerty
对于在对象原型链上某处定义的方法,同样的概念也适用。如果该方法存在于一个对象的原型链上,那么 this 指向的是调用这个方法的对象,就像该方法就在这个对象上一样。
// 对象 p 没有属于它自己的 f 属性,它的 f 属性继承自它的原型。虽然最终是在 o 中找到 f 属性的,这并没有关系;查找过程首先从 p.f 的引用开始,所以函数中的 this 指向 p 。也就是说,因为 f 是作为 p 的方法调用的,所以它的 this 指向了 p 。var o = { f: function () { return this.a + this.b; },};var p = Object.create(o);p.a = 1;p.b = 4;console.log(p.f()); // 5
相同的概念也适用于当函数在一个 getter 或者 setter 中被调用。用作 getter 或 setter 的函数都会把 this 绑定到设置或获取属性的对象。
function sum() { return this.a + this.b + this.c;}var o = { a: 1, b: 2, c: 3, get average() { return (this.a + this.b + this.c) / 3; },};Object.defineProperty(o, "sum", { get: sum, enumerable: true, configurable: true,});console.log(o.average, o.sum); // 2, 6
当函数被用作事件处理函数时,它的 this 指向触发事件的元素(一些浏览器在使用非 addEventListener 的函数动态地添加监听函数时不遵守这个约定)。
// 被调用时,将关联的元素变成蓝色function bluify(e) { console.log(this === e.currentTarget); // 总是 true // 当 currentTarget 和 target 是同一个对象时为 true console.log(this === e.target); this.style.backgroundColor = "#A5D9F3";}// 获取文档中的所有元素的列表var elements = document.getElementsByTagName("*");// 将bluify作为元素的点击监听函数,当元素被点击时,就会变成蓝色for (var i = 0; i < elements.length; i++) { elements[i].addEventListener("click", bluify, false);}
当代码被内联 on-event 处理函数 调用时,它的 this 指向监听器所在的 DOM 元素:
<button onclick="alert(this.tagName.toLowerCase());">Show this</button><!-- 在下面这种情况下,没有设置内部函数的 this,所以它指向 global/window 对象(即非严格模式下调用的函数未设置 this 时指向的默认对象)。 --><button onclick="alert((function(){return this})());">Show inner this</button>
分析一道题
class Test { prop = { func1: function () { console.log(this); }, func2: () => { console.log(this); }, };}const t = new Test();t.prop.func1(); // object propt.prop.func2(); // object tconst { prop } = t;prop.func1(); // object propprop.func2(); // tconst { func1, func2 } = prop;func1(); // undefinedfunc2(); // t
答案已经公布,想想为什么呢?
]]>地球已经出现几十亿年,人类出现不过区区几百万年,而且真正开始在地球占领统治地位的时间则更短了。有时候我也时常在想,为什么我感觉现在人的生活并没有因为人类统治地位而更轻松?很明显,近现代一两百年的科技发展,将人类带上了一个新的台阶,我觉得这个变化不亚于人类第一次用火和人类第一次有国家、文明的概念。按理说,科技如此发展,人类不应该更加团结、和谐的发展吗?而现在这个世界却还存在战争、饥荒、种族歧视等等各种理应不出现在这个时代的东西。我想多思考思考。
「科学发展快而人类发展慢」,最主要的,我认为就是这个原因了。有点德不配位的感觉。人类普及了电、石油等基础的元素,于是飞速发展了火车、轮船、飞机、电脑等各种缩短时间、空间的工具。那人类的本质有因为这些工具而改变吗?有,但是微不足道。
因为这些工具,人类爆发了资源的掠夺战,爆发了有史以来最大的战争,发生了南京大屠杀、广岛与长崎原子弹等的人类悲剧。现阶段人类生活看似平稳,实际上也是非常脆弱的,中东现在还陷入战争泥沼,非洲饥荒灾难不断。
因为这些工具,资本主义更加横行霸道,富的更富,穷的更穷。才会发生如现在这样,资本主义光明正大的宣传 996 ,竟然称之为福报,底层人民的需求得不到关注,花几十年工作,才能在大城市买房,社会矛盾越来越凸显。
看似平稳的社会其实也是脆弱的很,一旦发生一些难以预测的灾难,例如传播性极强、致死率极高的疾病,难以想象的饥荒灾难等,世界一定会再陷入混乱,生灵涂炭未必不会再发生在地球,作为普通人,也只能默默祈祷了。如果真的发生这一切,感觉以目前人类的程度,是难以抵抗的。希望有招一日,人类真的能够大一统,抛开歧视,人人都能克服自己的虚荣、欲望,成为桃花源中的人,那个时候,人类就可以抵御大部分的灾难了。
长路漫漫,希望世界永远和平,🙏。
]]>采用 Jest 对 react 项目进行单元测试。
曾使用 Mocha 对项目进行过单元测试,但是 Mocha 需要配合一系列工具包( sinon, enzyme, chai, nyc )等,加上 Mocha 对 typescript 支持不是特别友好。所以打算采用 Jest 进行单元测试,目前的测试工具 Mocha、Jest、Ava 区别大致如下:
框架 | 断言 | 异步 | Mock | 代码覆盖率 |
---|---|---|---|---|
Mocha | 不支持(Chai/power-asset) | 友好 | 不支持(Sinon) | 不支持(Istanbul) |
Jest | 默认支持 | 友好 | 默认支持 | 支持 |
Ava | 默认支持 | 友好 | 不支持(Sinon) | 不支持(Istanbul) |
安装所需依赖(采用的 typescript )
yarn add -D jest babel-jest jest-transform-stub ts-jest enzyme-to-json enzyme enzyme-adapter-react-16
配置命令
..."scripts": { "test": "cross-env jest", "test:with-coverage": "cross-env TEST_COVERAGE=true jest" },...
根目录下编辑 jest.config.js
module.exports = { preset: "ts-jest", // 采用 ts 测试 setupFiles: ["./jest.setup.js"], transform: { "^.+\\.[tj]s?$": "babel-jest", "^.+\\.[tj]sx?$": "babel-jest", "^.+\\.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2|svg)$": "jest-transform-stub", }, moduleNameMapper: { "^@/(.*)$": "<rootDir>/src/$1", // 用 @ 映射当前 src 下的路径 "^.+.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2|svg)$": "jest-transform-stub", // stub 掉 css|styl|less|sass|scss|png|jpg|ttf|woff|woff2|svg 的测试 "\\.worker.entry.js": "<rootDir>/__mocks__/workerMock.js", }, globals: { "ts-jest": { tsConfig: "./tsconfig.test.json", // 指定 ts 测试配置文件 }, }, collectCoverage: process.env.TEST_COVERAGE ? true : false, // 是否需要查看测试覆盖率 collectCoverageFrom: ["<rootDir>/src/**/*.{ts,tsx}", "!**/node_modules/**"], testPathIgnorePatterns: ["<rootDir>/dist/", "<rootDir>/node_modules/"], snapshotSerializers: ["enzyme-to-json/serializer"], transformIgnorePatterns: ["<rootDir>/dist/", "<rootDir>/node_modules/"], testURL: "http://localhost", clearMocks: true,};
根目录下编辑 jest.setup.js
// 该文件是运行单元测试前会执行一遍,可以 mock 掉一些报错的东西const React = require("react");if (typeof window !== "undefined") { global.window.resizeTo = (width, height) => { global.window.innerWidth = width || global.window.innerWidth; global.window.innerHeight = height || global.window.innerHeight; global.window.dispatchEvent(new Event("resize")); }; global.window.scrollTo = () => {}; if (!window.matchMedia) { Object.defineProperty(global.window, "matchMedia", { value: jest.fn((query) => ({ matches: query.includes("max-width"), addListener: jest.fn(), removeListener: jest.fn(), })), }); } const mockResponse = jest.fn(); Object.defineProperty(window, "location", { value: { hash: { endsWith: mockResponse, includes: mockResponse, }, assign: mockResponse, }, writable: true, });}const Enzyme = require("enzyme");const Adapter = require("enzyme-adapter-react-16");Enzyme.configure({ adapter: new Adapter() });Object.assign(Enzyme.ReactWrapper.prototype, { findObserver() { return this.find("ResizeObserver"); }, triggerResize() { const ob = this.findObserver(); ob.instance().onResize([{ target: ob.getDOMNode() }]); },});console.log("Current React Version:", React.version);
至此 Jest 项目搭建就已经完成了, 比 Mocha 简单些。
编写测试文件
import React from "react";import { App as TargetComponent } from "../App";import { shallow } from "enzyme";describe("src > APP", () => { const defaultProps = { updateTabs: jest.fn(), // 模拟 updateTabs 函数 user: {}, }; // shallow 浅拷贝模拟 react 组件 const render = (props: {} = {}) => shallow(<TargetComponent {...defaultProps} {...props} />); // describe 一个方法 describe("componentDidMount", () => { // 判断行为 it("should direct return if no tab", () => { // 实例化组件 const component = render({ user: { permissionsMapping: { "/": null, }, }, }); // 执行方法 const result = component.instance().componentDidMount(); // 执行断言 expect(component.instance().props.updateTabs).toBeCalledWith("test"); expect(result).toBeUndefined(); }); });});
执行单元测试
yarn test
或者执行单个测试文件
# 采用 describe.only 方法不行,略坑node_modules/.bin/jest src/components/BaseList/__test__/index.test.tsx
查看覆盖率
yarn test:with-coverage
在跟目录下 coverage 下会生成 html 文件,在浏览器打开查看覆盖率即可,可以在 Jest 配置中配置测试率要求,具体可以查看官网文档。
个人感觉 Jest 比 Mocha 配置简单些,易上手,Mocha 需要配合 sinon 进行 stub,配合 Chai 断言效果才会更好,在使用上 Jest 和 Mocha 并无太大区别。
(完)
]]>如下为一段代码, 请完善 sum 函数, 使得 sum(1, 2, 3, 4, 5, 6) 函数返回值为 21 , 需要在 sum 函数中调用 asyncAdd 函数, 且不能修改 asyncAdd 函数:
/** * 请在 sum函数中调用此函数, 完成数值计算 * @param {*} a 要相加的第一个值 * @param {*} b 要相加的第二个值 * @param {*} callback 相加之后的回调函数 */function asyncAdd(a, b, callback) { setTimeout(function () { callback(null, a + b); }, 100);}/** * 请在此方法中调用asyncAdd方法, 完成数值计算 * @param {...any} rest 传入的参数 */async function sum(...rest) { // 请在此处完善代码}let start = window.performance.now();sum(1, 2, 3, 4, 5, 6).then((res) => { // 请保证在调用sum方法之后, 返回结果21 console.log(res); console.log(`程序执行共耗时: ${window.performance.now() - start}`);});
题目是在掘金上看到的, 立马开始动手做题, 第一时间想到的就是使用剩余参数, 然后配合 promise 做递归, 下面是我的实现方式:
async function sum(...rest) { const [a = 0, b = 0, ...others] = rest; return new Promise(function (resolve, reject) { asyncAdd(a, b, function (arg1, callBackResult) { if (others.length !== 0) { sum(callBackResult, ...others).then((res) => { resolve(res); }); } else { resolve(callBackResult); } }); });}
思路很简单, 在 sum 函数中, 返回一个 promise , 如果剩余参数的长度为 0 , 则直接 resolve asyncAdd 函数执行的回调的结果, 否则递归调用 sum, 把结果作为第一个 sum 的第一个参数, 剩余参数直接展开, 然后在递归调用的回调中, 再 resolve 结果, 最终达到题目要求。
比较满意的看了看自己的代码, 觉得不错, 然后看了一下标准答案, 发现, 自己的代码执行速度还是太慢, 看一下别人的实现:
// 方法一, 和我的差不多, 但是比我的更简洁。。。async function sum1(...rest) { let result = rest.shift(); for (let num of rest) { result = await new Promise((resolve) => { asyncAdd(result, num, (_, res) => { resolve(res); }); }); } return result;}// 方法二, 重点在于把参数两两分开, 使用 Promise.all 一次执行多个 promiseasync function sum2(...rest) { if (rest.length <= 1) { return rest[0] || 0; } const promises = []; for (let i = 0; i < rest.length; i += 2) { promises.push( new Promise((resolve) => { if (rest[i + 1] === undefined) { resolve(rest[i]); } else { asyncAdd(rest[i], rest[i + 1], (_, result) => { resolve(result); }); } }) ); } const result = await Promise.all(promises); return await sum(...result);}// 方法三, 隐氏类型转换 和 promise.all 结合使用async function sum(...rest) { let result = 0; // 隐氏类型转换, 对象 + 数字, 会先调用对象的toString 方法 const obj = {}; obj.toString = function () { return result; }; const promises = []; for (let num of rest) { promises.push( new Promise((resolve) => { asyncAdd(obj, num, (_, res) => { resolve(res); }); }).then((res) => { // 把回调的结果直接给 result, obj下次计算的时候, 会自动调用 toString 方法拿到最新的 result result = res; }) ); } await Promise.all(promises); return result;}
以上几种结果, 方法三是最快的, 本想给我的方法也做做优化, 发现我的方法确实没办法优化, 因为我是直接在 sum 里面返回 promise , 没有办法使用 promise.all 。 究其原因, 是因为我第一时间就想到用递归, 而不是 for 循环, 我发现在敲代码的过程中, 类似循环的问题, 能用递归的地方, 我第一想到的方案都是采用递归, 很少很少采用 for 循环去做某件事, 这是为什么呢?看来以后还是得转变一下思路, 递归的效率有的时候不一定是最高的。
]]>树叶的一生,只是为了归根吗? – 亚索
每当想起人生的意义的时候,我总是会想起 LOL 中亚索的这句话「树叶的一生,只是为了归根吗?」。我觉得他说的这句话,似乎适合让任何一个失落的人发出这样的呐喊,同时也会产生共鸣,人的一生,其实不也像一片树叶吗?
树叶源自于树枝,春天发芽,夏天茂盛,秋天落叶,冬天归根。而大多数人,少年时在家乡出生和长大,年轻时外出学习和工作,中年时成家立业,准备退休,年老时解甲归田。人生代代如此,反复循环,那人生究竟有何意义?
有人希望大富大贵,有人希望平平淡淡一生,有人希望走遍世界。为什么大家会有这么多不同的想法?我其实一直认为人类,是一种特殊的动物,动物有的行为,人类基本都有。但唯独,好奇与恐惧是人类特有的。七情六欲,动物应该也有这样的行为,但是好奇心和恐惧心里远远不如人类。为什么有人喜欢大富大贵?他好奇那种拥有花不完的钱的感觉。为什么喜欢拥有强大的权力?他好奇那种强大权力的感觉。为什么想环游世界?他好奇那种走遍世界的感觉。为什么有人希望平平淡淡一生?因为恐惧接受一些事物。为什么你能在电影中看到有些人为了活下去,做出各种匪夷所思的事情?因为恐惧死亡。
基本上,人类的行为大部分在动物身上也发生过,但唯独好奇与恐惧就像是人类特有的。好奇使人类进化的更聪明,恐惧使人类更加珍惜生命。如果生活中凡事发生了一些难以理解的事情,你只要把这件事当做发生在动物身上,基本就能说的通了。还真是有点细思极恐。
年少时,我一直以为这个世界有鬼怪和神仙存在,我以为只有他们是永远存在的。恐怕,世界上,没有什么生命是永远存在的,人的一生短短几十年,其实脆弱的很。
为什么我今天会写这么沉重的话题?一方面是因为我很早就自己想过这件事,另一方面是因为前两天去医院检查了一下腰,发现自己腰椎峡部断裂和腰椎滑脱。我相信大部分人,都应该把自己的生命看的非常非常重要,在没有大灾大难的时候,可能总觉得自己能活非常久,其实是我们大意了,生命发生意外的时候,很有可能招呼都不会和你打。我很早前做过一个手术,那种恐惧感可能这辈子都不会忘记,所以当医生说我的腰很严重的时候,我的心里有那么一瞬间非常失落,感觉就像是给我判了死刑一样。其实这还只是中等情况,不至于说威胁生命安全,但是如果真的有一天,人被宣告得了不治之症,有多少人能承受得住这个打击呢?
我们总觉得发生在别人身上的,很难在自己身上重复,离自己太遥远,其实这就发生在身边。愿所有人,能早点意识到生命的脆弱,或许很多不必要发生的事情,就会少很多。
]]>针对 web 前端优化方案的总结, 主要技术栈为 React。
与以往 PHP, JSP 等服务端渲染不同, 现如今大多数 web 端采用 React, Vue, Angular 等客户端渲染方案。单页应用带来的好处是显而易见的, 前端开发人员可以专注于前端页面的交互, 后端人员专注于数据的处理, 分工明确。另一方面得益于 nodejs 的生态, 例如 npm 包管理, webpack, gulp 打包等, 前端开发人员可以避免重复造轮子, 开发也变得越来越迅速。然而单页应用也带来另一个问题, 随着引入的库越来越多, 项目也越来越臃肿, 页面加载速度奇慢无比, 本文主要讨论针对 webpack 手动搭建的 React 项目为例做优化。
首先简单罗列一下 web 开发中可以优化的点有哪些:
下面依次讲解。
这是最简单粗暴的, 打包后的资源文件, 如果不做任何优化, 大部分项目中压缩后的 vender.xxx.js 文件, 大概会有 5-8 M,在国内采用 CDN 加载, 时间大概 1 - 2 s 左右, 在次基础上, 还可以进行 GZip 压缩, 以 阿里 oss 为例子, 采用 GZip 压缩后的资源, 大小会差不多减少三分之二, 访问时间会在 1 秒左右, 如果对项目访问速度要求不高, 这完全足够了。
另外, 例如图片、视频、音频等大文件, 肯定是要采用 CDN 的, 可能还要引入流的概念, 这里就不累述了(关键我也不熟悉)。
SVG 的优势(来源谷歌):
另外还有些个人认为的优势, SVG 可以做动画, 可以嵌入 HTML 文件, 减少 HTTP 请求。
针对样式, 大部分项目中会使用 less,sass 或者 stylus, 通过 less-loader
, sass-loader
等各种 loader, 最后基本有两种方案, 一是通过 css-loader
和 style-loader
把生成后的样式嵌入 HTML, 作为 style 标签引入, 另一种是通过 css-loader
和 miniCssExtractPlugin
生成压缩后的 css 引入。两种方式我觉得都没什么问题, 差不多, 但我更喜欢后者。嵌入到 HTML 可以减少 HTTP 请求, 但是压缩后的样式再经过 GZip 压缩其实也就几十毫秒左右。
代码优化根据项目采用的框架不同, 优化方案也不同, 但万变不离其宗, 主要是减少重渲染次数。
以 React 为例, 减少 componentWillReceiveProps
, 使用 hooks 等, 注意 state 的值, 注意什么时候该用 props, 什么时候该用 state。props 和 state 的改变都会引起 re-render , 我的总结就是: 「 在组件中, 这个值会经常改变, 再考虑
把这个值设为 state 。」 其实也不一定准确, 还是得看具体情况, 比如, editable
, visible
等这类表示某些状态的, 大多数时候都作为 state, 但也有例外, 比如 loading
, 业务不一样, 可能在会引入 redux
全局状态, 在发生 HTTP 请求的时候, 设置 loading
为 true, 组件可以从全局去拿这个状态值。
GZip 在上述方案中多次提到, 如果你不想使用 CDN, 就想部署在自己的服务器中, 开启 GZip 根据不同的 web 容器, 设置的方式也不一样, 但总体思路差不多。
先说 nodejs , 基于 nodejs 的 GZip , 根据不同的 nodejs 框架有不同的使用方案, 我使用的是 koa, 引入的是 koa-compress
, 使用方式如下:
...const compress = require("koa-compress");const Koa = require("koa");const server = new Koa();server.use( compress({ threshold: 1024, gzip: { flush: require("zlib").Z_SYNC_FLUSH, }, deflate: { flush: require("zlib").Z_SYNC_FLUSH, }, br: false, }));...
Nginx, Apache 等 web 容器, 编辑 conf 文件即可, 以 Nginx 为例:
...# 开启gzipgzip on;# 启用gzip压缩的最小文件, 小于设置值的文件将不会压缩gzip_min_length 1k;# gzip 压缩级别, 1-9, 数字越大压缩的越好, 也越占用CPU时间, 后面会有详细说明gzip_comp_level 1;# 进行压缩的文件类型。javascript有多种形式。其中的值可以在 mime.types 文件中找到。gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png application/vnd.ms-fontobject font/ttf font/opentype font/x-woff image/svg+xml;# 是否在http header中添加Vary: Accept-Encoding, 建议开启gzip_vary on;# 禁用IE 6 gzipgzip_disable "MSIE [1-6]\.";# 设置压缩所需要的缓冲区大小gzip_buffers 32 4k;# 设置gzip压缩针对的HTTP协议版本, 没做负载的可以不用# gzip_http_version 1.0;...
Apache 类似。
使用 webpack.DllPlugin
, 可以把常用的且基本不变的库, 单独拆分出去, 且仅需 build 一次, 可以提升打包的速度。
// webpack.dll.config.jsconst webpack = require("webpack");const path = require("path");const CopyWebpackPlugin = require("copy-webpack-plugin");module.exports = { entry: { react: ["react", "react-dom"], }, mode: "production", output: { filename: "[name].dll.[hash:6].js", path: path.resolve(__dirname, "dist", "dll"), library: "[name]_dll", }, plugins: [ new webpack.DllPlugin({ name: "[name]_dll", path: path.resolve(__dirname, "dist", "dll", "manifest.json"), }), ],};
在项目中使用:
// webpack.config.js...new webpack.DllReferencePlugin({ manifest: path.resolve(__dirname, 'dist', 'dll', 'manifest.json'), }),new CleanWebpackPlugin({ cleanOnceBeforeBuildPatterns: ['**/*', '!dll', '!dll/**', '!pdfjs', '!pdfjs/**'],}),...
第一次使用 webpack.dll.config.js 文件会对第三方库打包, 打包完成后就不会再打包它了, 然后每次运行 webpack.config.js 文件的时候, 都会打包项目中本身的文件代码, 当需要使用第三方依赖的时候, 会使用 DllReferencePlugin 插件去读取第三方依赖库。所以说它的打包速度会得到一个很大的提升。
通过 splitChunks, 会把这些库再单独拆分出去
const merge = require("webpack-merge");const baseConfig = require("./webpack.config.js");const opimizeCss = require("optimize-css-assets-webpack-plugin");const TerserPlugin = require("terser-webpack-plugin");// 打包后文件大小分析const BundleAnalyzerPlugin = require("webpack-bundle-analyzer") .BundleAnalyzerPlugin;module.exports = merge(baseConfig, { mode: "production", optimization: { runtimeChunk: { name: "manifest", }, splitChunks: { maxInitialRequests: 10, cacheGroups: { vendor: { priority: 1, name: "vendor", test: /node_modules/, chunks: "initial", minSize: 0, minChunks: 1, }, moment: { name: "moment", priority: 5, test: /[\/]node_modules[\/]moment[\/]/, chunks: "initial", minSize: 100, minChunks: 1, }, lodash: { name: "lodash", priority: 6, test: /[\/]node_modules[\/]lodash[\/]/, chunks: "initial", minSize: 100, minChunks: 1, }, antd: { name: "antd", priority: 7, test: /[\/]node_modules[\/]antd[\/]es[\/]/, chunks: "initial", minSize: 100, minChunks: 1, }, }, }, minimizer: [ new opimizeCss(), new TerserPlugin({ cache: true, parallel: true, sourceMap: true, }), ], }, plugins: [new BundleAnalyzerPlugin()],});
理论上你可以对你的任何代码使用懒加载, 但我觉得仅对页面级别使用懒加载足以, 通过路由懒加载界面:
//// 路由列表export const routesMapping = { // // 首页 "/": React.lazy(() => import("@/pages/welcome")), "/welcome": React.lazy(() => import("@/pages/welcome")),};
...{ map(routesMapping, (Component, key) => { return ( get(permissionsMapping, key) && ( <Route exact path={key} > <Component {...this.props} /> </Route> ) ); });}...
至此, 生产环境下打包后, 你的 js 文件将会分散为多个。
经过以上的各种优化, 界面的访问速度已经很快了, 还有一个针对本地缓存的方案, 这个与开启 GZip 压缩类似, 以 nodejs 为例:
...server.use(async (ctx, next) => { if (ctx.request.path.indexOf('/api') === -1) { ctx.set('Cache-Control', 'public'); } else { ctx.set('Cache-Control', 'no-store, no-cache, must-revalidate'); } ctx.set('max-age', 7200); await next();});...
对除了 api 请求以外的请求进行缓存, 除了第一次访问需要网络请求资源以外, 下一次刷新将直接从本地缓存获取资源。
优化 web 体验, 基本上是围绕 减少资源大小( 压缩, 拆分 )
, 减少HTTP请求( svg , 样式优化 )
, 避免重绘和重排( 优化代码 )
, 提高 HTTP 访问速度( CDN )
, 如果大家有更好的优化方式, 欢迎一起讨论。
这两天确实有点开始焦虑了,准确的来说,我好像一直都很焦虑。最近又有新想法了,想重新捡起 Android ,想做 APP 开发。回想起大学的时候,我一直都好想做 APP 开发,书也买了不少,视频也看了不少,但始终没有坚持下去。刚刚重新看了点关于 Android 的知识,回想起以前的一些‘经验’,不知怎的,有点黯然伤神。
当初为什么没有坚持下去呢?我一直觉得自己是个很浮躁又很有想法的人,但也正是这种想法让我始终难以在某个领域特别突出。从上大学开始,到现今算算也已经五年了,如果我就一直在做 Android 开发,到今天会怎么样呢?我会不会是这个行业比较牛的人了?我觉得很有可能会。
实际上我并非是个没有计划的人,相反,我非常喜欢做一些计划,也许计划没有完成,但是计划总能让事情做起来井然有序。我曾在大学的某天心里默默列下我的第一个五年计划,就是大学毕业的那年,存下五万块钱,虽然有点俗气,但好歹也算是完成目标了。我给自己的五年计划往往无关技术、工作、爱情等。很俗,可以说只有钱和人生,像国家的中心目标一样,要以经济建设为中心,如果离开经济,其它一切可能都是浮云。我的下一个人生五年计划,也非常简单,30 岁之前,结婚、生子、存款 30 万。不再不切实际,丢掉幻想。
说的有点远了,我今天想说的是关于坚持一个行业的五年,我希望我能够坚持某个行业领域 5 年,然后在这个行业里面技术属于全国 20% 以上的水准。可惜的是我还没有确定好目标,这是非常非常不好的。我会一直坚持做前端 5 年吗?我会一直做医疗相关的系统吗?我会一直在广州做 IT 发展吗?这些我都不能确定。但我能确定一个目标,就是我想做自由职业者。而我给自己定义的工作是,不希望给任何人打工,我希望运营某些属于自己的产品,有着不错的收入,带着老婆孩子各个城市居住。
人生可没有多少个五年十年可以奋斗,有句话说的好,「种下一棵树最后的时间是十年前,其次是现在」
。我想在现在种下一颗树的种子,在五年后看看结果如何。为了自由职业而奋斗,当下我要做的应该就是一直保持奋斗精神,一直学习下去。
有时候我一直怀疑自己不适合做一个程序员,出身普普通通,小学六年从未接触过英语,接触计算机也是因为去网吧打游戏。小时候的我像大多数的人的童年一样,希望以后做个科学家,从没想过去做程序员,那时候连程序员这个职业是什么都不知道。一直到高中,吵着跟爸妈说要买电脑,说是为了学习,实则是为了打游戏。最疯狂的时候,痴迷到什么程度呢?高一高二每次开运动会的时候,我都是在家打游戏度过的。基本不参加一些这样的活动,每天中午回家吃饭的时候,要是没人管着,也会开一把游戏。每年的元旦都放一天假,年年都去网吧通宵。经常晚自习后,还去网吧打两把游戏。(主要是英雄联盟)
那个时候互联网还不发达,智能手机也刚刚起步,马云的支付宝和淘宝也在那个时候开始火起来,比特币大约 200 多 RMB,虽然那个时候是高中生,但我总希望有一天靠电脑挣钱。在网上查资料,还用自己家的电脑去挖矿,虽然那个时候啥也不懂,但在网上看到说这个值钱。后来,高三毕业,高考总分 500 分,数学 120 多,语文 100 多,英语 120 多,理科才 150 几分,进入了一所二本学校,那个时候不顾任何人反对,就是要填计算机专业。
在大学里面,学的关于计算机的理论知识太多,且丝毫难以提起学习的兴趣,只能花时间自学,而且还是在第二年才认真开始学习。我觉得这跟大学也有非常大的关系,当然了,主要还是自己的问题,就不展开细说了。
以上是我的背景,就目前为止,我主要有哪些焦虑呢?
我觉得一切的一切,都是为了生活,不管哪个行业。你想要好的生活,生活成本就会相应的变高,那就必须要好好工作,这是一切努力的前提。
现在在国内,只要四肢健全,随便找份工作,应该就饿不死,但是大部分人应该都不仅仅想要饿不死吧。虚荣、贪婪等等欲望放大了生活成本。
为了提高工资待遇,前往一线城市打拼,生活节奏就越来越快,就拿自己举例,我发现我的时间变得越来越少,每天都感觉时间不够用。
每天三点一线,宿舍 - 上班/下班的路上 - 公司,虽然在楼下有办健身卡,但是难以提起动力,下班后就只想回家洗个澡躺着。在公司每天又坐着,腰又累。长此以往,恶性循环。
在大学的时候,我学了汇编、C、C++、Java、安卓、PHP,还研究过微信小程序。学的杂,又学的不深,简直就是一个野生程序员,野蛮生长。
与其它行业不一样,程序员是要一直保持学习的,倘若长时间不学习,动手能力就会衰退,这是对程序员最致命的一点,
我相信大部分的人应该意识到了这一点。昨天想学 ReactNative,今天又想学 flutter,明天又想学 kotlin。这简直就是我的写照,我想学好多好多东西,但说实在的,自己的学习能力和坚持能力,只能处于一个中等水平。导致自己在技术上的焦虑更深。
我现在还是处于这个不定漂浮的状态,我还是没有找到解决的方式处理自己的焦虑,感觉自己就像是处于一个不断恶性循环的牢笼中。我不知道该怎么办,但是,我告诫自己,不要走极端。想起曾经看过的一句话,如果不知道该做什么的时候,就去看书吧,这是唯一不会做错的事情
。嗯,于是我开始学起日语了(英语都还没学好,也不知道自己是咋想的)。我不断告诉自己,焦虑是没有用的,只能自己体会这份焦虑和孤独,时间会磨平这一切,我想我要做的应该就是默默努力,不要放弃自己,等待时机。
突然有那么一刻,我想写点什么,关于人生的一些思考。本来我曾希望只在博客里面写关于技术的东西,但是有的时候,我觉得总想写点什么才行,脑海里时不时会冒出一些 idea,如果不写下来,没过多久我觉得我就会忘记。所以,这将是我写这个系列的初衷,观点不一定正确,但从我开始写的时候,我也没想过让多少人看,也许根本没有人看,但这无关紧要,几十年后,让年纪大了的自己回头看看曾经 20 多岁的自己的一些想法,我想应该也是及其有趣的。
为什么叫「 未来的路 」?我没有指明主语,所以我想写的这个系列,肯定不是只关于自己,这条路,也许关于在看这篇文章的你,或者你熟知的某个人,或者是我,亦或者是整个人类。受阮一峰老师的启发,他有一个系列名叫「 未来世界幸存者 」 ,看完后我也给我的这个系列取名为「 未来的路 」。
我觉得内容将以生活中的小事为主,所谓见微知著,一些小事往往能展现一些大的人生与智慧。其次以国际政治、历史、文化等为背景,可能会谈到一些关乎人类命运的东西,我谈这些主要不是为了要说明什么,更多地可能是关于人性,在我眼里,我觉得有的时候人类与动物并无二样,通过所见所闻,剖析人类人性,直面人的本性,我觉得这也正是我想记录下来的。
我也是临时起意,感觉并无具体的计划,也许写个三五年,也许写个八年十年,想到哪写到哪,随心所欲,唯一一点对自己的要求就是不写反人类的内容,在所有文章中,保持最中立的一面。
]]>通过 proxychains-ng 实现 terminal 代理
目前的代理功能,大部分底层都是基于 socks5、http 等,然后配合插件如 SwitchyOmega 等,实现在浏览器端代理,或者是全局代理,但问题是在 terminal 下仍然不走代理。即使通过
export http_proxy=http://127.0.0.1:1081 https_proxy=http://127.0.0.1:1081
发现还是不行。这几天终于忍受不了,我觉得一定有人跟我一样的想法,肯定有人已经着手在做这件事情。果然,偶然发现 proxychains-ng 这款开源软件,决定试一下。
经过昨天的实践,经过一顿操作后,发现不行,然后就果断关机下班了。今早到公司再打开 terminal 试下,发现昨天已经成功了。
重启 Mac,按住 Option 键进入启动盘选择模式,再按 ⌘ + R
进入 Recovery 模式。实用工具( Utilities )-> 终端( Terminal )。输入命令 csrutil disable
运行。重启进入系统后,终端里输入 csrutil status
,结果中如果有 System Integrity Protection status:disabled
。则说明关闭成功。
brew install proxychains-ng
brew 安装后,proxychains-ng 的配置文件在 /usr/local/etc/proxychains.conf
下,在文件最后
......# 找到 ProxyList[ProxyList]# 配置本地已经有的 socks5代理socks5 127.0.0.1 1086
运行命令
proxychains4 curl ipinfo.io
输出结果如下:
[proxychains] config file found: /usr/local/etc/proxychains.conf[proxychains] preloading /usr/local/Cellar/proxychains-ng/4.14/lib/libproxychains4.dylib[proxychains] DLL init: proxychains-ng 4.14[proxychains] Strict chain ... 127.0.0.1:1080 ... ipinfo.io:80 ... OK{ "ip": "103.121.211.104", "city": "Tokyo", "region": "Tokyo", "country": "JP", "loc": "35.6895,139.6917", "org": "AS4785 xTom", "postal": "151-0052", "timezone": "Asia/Tokyo", "readme": "https://ipinfo.io/missingauth"}
每次使用都需要输入 proxychains4
,显得太长了,增加别名在 .zshrc 下进行优化,
# 增加 aliasalias out='proxychains4'
此后,每次在需要翻墙的情况下,只需要在命令前,加上 out
即可。
当然 proxychains-ng 还有更加丰富的功能,貌似可以实现任意软件的翻墙,但由于我不需要,也没有去研究这个,以后有需要再说吧。
]]>本文是根据 PSR 规范英文文档翻译而来,建议多次阅读以便熟悉这些规范,并在工作中用到这些规范。
一篇翻译而来的 PSR-1 规范
通过翻译 PSR 规范,掌握 PHP 的开发规范
本文中的关键词 "必须"
, "禁止"
, "必要"
, "最好"
, "最好不要"
, "应该"
, "不应该"
, "建议"
, "可以"
, "可选"
应按照 RFC 2119 的规定进行解释。
PHP 文件必须
使用 <?php
或者 <?=
标签开始
PHP 文件必须
使用不带 bom 的 UTF-8 编码
PHP 文件应该
要么只声明类、函数、变量等,要么引起副作用(例如生成输出,改变 .ini 配置文件等操作),但是不应该
两者都做。
命名空间和类必须
遵循自动加载规范 [PSR-0, PSR-4]
PHP 类名必须
以大驼峰规范命名,例如 HomeClass
PHP 类文件中的常量必须
使用下划线分隔且以大写形式声明,例如 APP_KEY
方法名必须
以小驼峰规范命名,例如 updateUser
PHP 便签必须
使用 <?php ?>
或者 <?= ?>
, 禁止
使用其它标签代替。
PHP 代码必须
使用不带 Bom 的 UTF-8 编码
PHP 文件应该
要么只声明类、函数、变量等,要么引起副作用(例如生成输出,改变 .ini 配置文件等操作),但是不应该
两者都做。
「副作用」(side effects) 一词的意思是,通过包含文件,但不直接声明类、函数、常量等而执行的逻辑操作。
「副作用」包含却不仅限于:
以下是一个违反此规范的例子:
// 副作用: 根本 ini 设置ini_set('error_reporting', E_ALL);// 副作用: 加载文件include "file.php";// 副作用: 生成输出echo "<html>\n";// 声明function foo(){}
以下是一个符合此规范的例子:
// 声明 foo 函数function foo(){}// 有条件的声明不产生副作用if (! function_exists('bar')) { function bar() { }}
命名空间和类必须
遵循 PSR 自动加载规范:[PSR-0, PSR-4]。
根据规范,每个类都独立为一个文件,且命名空间至少有一个层次:顶级的组织名称(vendor name)。
类的命名必须
遵循 StudlyCaps 大写开头的驼峰命名规范。
PHP 5.3 及以后版本的代码必须
使用正式的命名空间。
例如:
// PHP 5.3 以后版本:namespace Vendor\Model;class Foo{}
5.2.x 及之前的版本应该
使用伪命名空间的写法,约定俗成使用顶级的组织名称(vendor name)如 Vendor_ 为类前缀。
// PHP 5.2.x 及更早版本:class Vendor_Model_Foo{}
此处的「类」指代所有的类、接口以及可复用代码块(traits)。
必须
使用下划线分隔且以大写形式声明,例如:namespace Vendor\Model;class Foo{ const VERSION = '1.0'; const DATE_APPROVED = '2012-06-01';}
类的属性命名可以
遵循:
应该
在一定的范围内保持一致。这个范围可以是整个团队、整个包、整个类或整个方法。方法名必须
为小驼峰命名
先整体快速阅读一遍,再回头看其中的配置。
使用开源工具sentry,自建一套 bug 监控系统。
在日常前端的开发中,当产品部署到线上的时候,前端页面一旦发生错误往往是非常严重,并且难以重现 bug ,为了使得项目中的 bug 可控,bug 监控就显得尤其重要了。然而,现在市面上的 bug 监控软件,可以说并不便宜,这个时候我们可以选择自建一套监控系统。
git clone git@github.com:getsentry/onpremise.git
配置文件上面都有相应的说明,这里就不赘述。
执行目录下的 install.sh 。(执行前请调到下一步看常见错误,节省时间。)
./install.sh
django.db.utils.OperationalError: could not connect to server: Connection refused
发生以上错误是由于数据库配置失败的问题,sentry 采用的是 postgresSQL ,我在安装的时候,数据库配置的是本机地址,之前也不怎么了解 postgresSQL ,下面是
postgresSQL 的一些安装配置
# 我采用的是 brew 安装,便于管理brew info postgresql@9.5# 注意安装后的说明...If you need to have postgresql@9.5 first in your PATH run: echo 'export PATH="/usr/local/opt/postgresql@9.5/bin:$PATH"' >> ~/.zshrcFor compilers to find postgresql@9.5 you may need to set: export LDFLAGS="-L/usr/local/opt/postgresql@9.5/lib" export CPPFLAGS="-I/usr/local/opt/postgresql@9.5/include"To have launchd start postgresql@9.5 now and restart at login: brew services start postgresql@9.5Or, if you dont want/need a background service you can just run: pg_ctl -D /usr/local/var/postgresql@9.5 start...# 这里执行以下几个命令即可# 输出到 zshecho 'export PATH="/usr/local/opt/postgresql@9.5/bin:$PATH"' >> ~/.zshrc# 更新环境source ~/.zshrc# 安装后我的文件目录为 /usr/local/var/postgresql@9.5# 编辑 postgresql.conf ,修改 listen_addresseslisten_addresses = '*' # what IP address(es) to listen on;# 编辑 pg_hba.conf ,修改 listen_addresses# 在文件最后添加host all all 0.0.0.0/0 md5# 启动 postgresqlbrew services start postgresql@9.5# 创建数据库用户createuser test -P# 创建数据库createdb bug-monitor# 登录数据库查看psql -U test# 进入后查看表\l# 可以看到已经存在 bug-monitor 表了
Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?
检查 docker 服务是否正常启动
(完)
]]>先整体快速阅读一遍,再回头看其中的配置。
通过 npm、webpack、babel、typescript 等工具,自己搭建一次 React 的 typescript 项目。
之前用过 next.js、ant-design-pro、create-react-app 等各种脚手架搭建过 react 项目,但是在使用过程中发现这些框架要么灵活性不足、要么打包后的文件过大等,所以决定手动搭建一次项目。
package.json 文件如下:
{ "name": "react-demo", "version": "1.0.0", "main": "index.js", "scripts": { "dev": "cross-env ENVIRONMENT_MODE=dev env-cmd node index.js", "start": "cross-env ENVIRONMENT_MODE=production env-cmd node index.js", "build": "webpack --config webpack.product.config.js", "build:dll": "webpack --config webpack.config.dll.js" }, "author": "kavience", "license": "ISC", "dependencies": { // babel 包 "@babel/core": "^7.10.2", "@babel/plugin-proposal-class-properties": "^7.10.1", "@babel/plugin-proposal-object-rest-spread": "^7.10.1", "@babel/preset-env": "^7.10.2", "@babel/preset-react": "^7.10.1", "@babel/preset-typescript": "^7.10.1", "babel-loader": "^8.1.0", "babel-plugin-import": "^1.13.0", // koa 相关的包,用于启动服务,代替 webpack-dev-server "koa": "^2.12.0", "koa-proxies": "^0.11.0", "koa-static": "^5.0.0", "koa2-connect-history-api-fallback": "^0.1.2", "koa-webpack": "^5.3.0", // react 相关 "react": "^16.13.1", "react-dom": "^16.13.1", "redux": "^4.0.5", "redux-thunk": "^2.3.0", // typescript相关 "typescript": "^3.9.5", "@types/react": "^16.9.35", "@types/react-dom": "^16.9.8", "@types/react-redux": "^7.1.9", "@types/react-router-dom": "^5.1.5", // webpack 相关 "webpack": "^4.43.0", "webpack-cli": "^3.3.11", "webpack-merge": "^4.2.2", "webpack-bundle-analyzer": "^3.8.0", "clean-webpack-plugin": "^3.0.0", "copy-webpack-plugin": "^6.0.2", "css-loader": "^3.5.3", "file-loader": "^6.0.0", "html-webpack-plugin": "^4.3.0", "less": "^3.11.3", "less-loader": "^6.1.0", "less-vars-to-js": "^1.3.0", "mini-css-extract-plugin": "^0.9.0", "optimize-css-assets-webpack-plugin": "^5.0.3", "postcss-loader": "^3.0.0", "url-loader": "^4.1.0", "compression": "^1.7.4", "cross-env": "^7.0.2", "env-cmd": "^10.1.0", "terser-webpack-plugin": "^3.0.3", // prettierrc 格式化,直接引用 umijs "@umijs/fabric": "^2.1.0" }}
以上这些包,直接网上搜这些就能明白其用处,在此就不赘述。
在根目录下新建 .babelrc 文件:
{ "presets": [ "@babel/preset-env", "@babel/preset-typescript", "@babel/preset-react" ], "plugins": [ "@babel/proposal-class-properties", "@babel/proposal-object-rest-spread", // 这里是配置 antd 按需加载 [ "import", { "libraryName": "antd", "style": true } ] ]}
{ "compilerOptions": { "outDir": "build/dist", "module": "esnext", "target": "esnext", "lib": ["esnext", "dom"], "sourceMap": true, "allowUnreachableCode": true, "allowUnusedLabels": true, "baseUrl": ".", "jsx": "preserve", "allowSyntheticDefaultImports": true, "moduleResolution": "node", "forceConsistentCasingInFileNames": false, "noImplicitReturns": true, "suppressImplicitAnyIndexErrors": true, "noUnusedLocals": true, "allowJs": true, "skipLibCheck": true, "experimentalDecorators": true, "strict": true, "paths": { // 这里是添加别名,用 @ 代替 src 目录 "@/*": ["./src/*"] }, "noEmit": true, "esModuleInterop": true, "resolveJsonModule": true, "isolatedModules": true }, "exclude": [ "node_modules", "build", "dist", "scripts", "acceptance-tests", "webpack", "jest", "src/setupTests.ts", "tslint:latest", "tslint-config-prettier" ]}
在根目录下新建 webpack.base.config.js:
const webpack = require('webpack');const path = require('path');const HtmlWebpackPlugin = require('html-webpack-plugin');const { CleanWebpackPlugin } = require('clean-webpack-plugin');const miniCssExtractPlugin = require('mini-css-extract-plugin');const fs = require('fs');const lessToJS = require('less-vars-to-js');const FilterWarningsPlugin = require('webpack-filter-warnings-plugin');// 这里是替换 ant-design 的less 样式变量,新建 variables.lessconst themeVariables = lessToJS(fs.readFileSync(path.resolve(__dirname, './src/assets/less/variables.less'), 'utf8'));module.exports = { entry: ['./src/index.tsx'], output: { filename: 'js/vendor.[hash].js', path: path.join(__dirname, '/dist'), publicPath: '/', }, resolve: { // 配置别名 alias: { '@': path.resolve(__dirname, 'src'), }, extensions: ['.ts', '.tsx', '.js'], }, module: { rules: [ { test: /\.(ts|js)x?$/, use: { loader: 'babel-loader', }, exclude: /node_modules/, }, { test: /\.(png|jpg|gif|svg|jpeg)$/, use: [ { loader: 'file-loader', options: { name: 'img/[name]_[hash:6].[ext]', }, }, ], }, { test: /\.css$/, use: [ { loader: miniCssExtractPlugin.loader, }, 'css-loader', ], }, { test: /\.less$/, use: [ { loader: miniCssExtractPlugin.loader, }, 'css-loader', { loader: 'less-loader', options: { lessOptions: { javascriptEnabled: true, modifyVars: themeVariables, }, }, }, ], }, ], }, plugins: [ // 如果没有按需加载,这句是为了忽略浏览器警告 new FilterWarningsPlugin({ exclude: /mini-css-extract-plugin[^]*Conflicting order between:/, }), // 配置 html new HtmlWebpackPlugin({ template: './public/index.html', favicon: './public/assets/favicon.png', }), // 采用 css 就好了,不用 style-loader ,把 css 统一压缩放入 dist/css 文件夹即可 new miniCssExtractPlugin({ filename: 'css/[name].css', }), // 配置 dll,基本不会修改的包,采用 dll 的方式引入, new webpack.DllReferencePlugin({ manifest: path.resolve(__dirname, 'dist', 'dll', 'manifest.json'), }), // 重新打包的时候,忽略这些文件 new CleanWebpackPlugin({ cleanOnceBeforeBuildPatterns: ['**/*', '!dll', '!dll/**'], }), ],};
在根目录下新建 webpack.dev.config.js,直接合并,设置 mode ,打开 source-map 即可。
const merge = require('webpack-merge');const baseConfig = require('./webpack.base.config.js');module.exports = merge(baseConfig, { mode: 'development', devtool: 'inline-source-map',});
在根目录下新建 webpack.production.config.js:
const merge = require('webpack-merge');const baseConfig = require('./webpack.base.config.js');const opimizeCss = require('optimize-css-assets-webpack-plugin');const TerserPlugin = require('terser-webpack-plugin');const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;module.exports = merge(baseConfig, { mode: 'production', optimization: { runtimeChunk: { name: 'manifest', }, // 拆分打包后的 js 文件 splitChunks: { cacheGroups: { default: { filename: 'common.js', chunks: 'initial', priority: -20, }, vendors: { chunks: 'initial', test: /[\\/]node_modules[\\/]/, filename: 'vendor.js', priority: -10, }, vendorsAsync: { chunks: 'async', test: /[\\/]node_modules[\\/]/, name: 'vendorsAsync', priority: 0, }, antv: { chunks: 'async', test: /[\\/]node_modules[\\/]@antv[\\/]/, name: 'antv', priority: 10, }, antd: { chunks: 'initial', test: /[\\/]node_modules[\\/]antd[\\/]/, filename: 'antd.js', priority: 20, }, moment: { chunks: 'async', test: /[\\/]node_modules[\\/]moment[\\/]/, name: 'moment', priority: 30, }, }, }, minimizer: [ new opimizeCss(), new TerserPlugin({ cache: true, parallel: true, sourceMap: true, }), ], }, // 打包后分析文件 plugins: [new BundleAnalyzerPlugin()],});
const webpack = require('webpack');const path = require('path');const CopyWebpackPlugin = require('copy-webpack-plugin');module.exports = { entry: { // 把 react、react-dom 作为 dll 文件引入 react: ['react', 'react-dom'], }, mode: 'production', output: { filename: '[name].dll.[hash:6].js', path: path.resolve(__dirname, 'dist', 'dll'), library: '[name]_dll', }, plugins: [ new webpack.DllPlugin({ name: '[name]_dll', path: path.resolve(__dirname, 'dist', 'dll', 'manifest.json'), }), ],};
const cp = require('child_process');const os = require('os');const path = require('path');const Koa = require('koa');const static = require('koa-static');const proxy = require('koa-proxies');const config = require('./webpack.dev.config.js');const koaWebpack = require('koa-webpack');const { historyApiFallback } = require('koa2-connect-history-api-fallback');// 从 process 中获取变量const { HOST_URL = 'http://api.example.com', FORM_DESCRIPTION_URL = 'http://api-form.example.com/', APP_PORT = 3333, ENVIRONMENT_MODE = 'dev',} = process.env;const isDev = ENVIRONMENT_MODE === 'dev';const server = new Koa();// 打包后的文件的目录const staticPath = './dist';// 代理白名单server.use(historyApiFallback({ whiteList: ['/api/*'] }));// 指向静态文件server.use(static(path.join(__dirname, staticPath)));// proxy 代理server.use( proxy('/api/form-descriptions(.*)', { target: FORM_DESCRIPTION_URL, changeOrigin: true, logs: true, secure: false, }),);server.use( proxy('/api/(.*)', { target: HOST_URL, changeOrigin: true, logs: true, secure: false, }),);// 获取本机地址function getIPAdress() { const interfaces = os.networkInterfaces(); for (const devName in interfaces) { const iface = interfaces[devName]; for (let i = 0; i < iface.length; i++) { const alias = iface[i]; if (alias.family === 'IPv4' && alias.address !== '127.0.0.1' && !alias.internal) { return alias.address; } } }}if (isDev) { const webpack = require('webpack'); const compiler = webpack(config); // 支持热更新 koaWebpack({ configPath: path.join(__dirname, '.', 'webpack.dev.config.js'), }).then((middleware) => { server.use(middleware); server.listen(APP_PORT, () => { console.log(`apiHostUrl: ${HOST_URL}, formDescriptionUrl: ${FORM_DESCRIPTION_URL}`); console.log(`App running at: http://localhost:${APP_PORT}`); console.log(`- Local: http://localhost:${APP_PORT}`); console.log(`- Network: http://${getIPAdress()}:${APP_PORT}`); if (isDev) { switch (process.platform) { case 'darwin': cp.exec(`open http://localhost:${APP_PORT}`); break; case 'win32': cp.exec(`start http://localhost:${APP_PORT}`); break; default: cp.exec(`open http://localhost:${APP_PORT}`); } } }); });} else { server.listen(APP_PORT, () => { console.log(`apiHostUrl: ${HOST_URL}, formDescriptionUrl: ${FORM_DESCRIPTION_URL}`); console.log(`App running at: http://localhost:${APP_PORT}`); console.log(`- Local: http://localhost:${APP_PORT}`); console.log(`- Network: http://${getIPAdress()}:${APP_PORT}`); });}
.env 文件主要是为了通过配置文件的方式配置变量。
# 端口APP_PORT=3333# api 接口地址HOST_URL="http://api.example.com"# 表单配置FORM_DESCRIPTION_URL="http://api-form.example.com"
再回到 script 命令:
{ "scripts": { // 开发模式 "dev": "cross-env ENVIRONMENT_MODE=dev env-cmd node index.js", // 生产模式 "start": "cross-env ENVIRONMENT_MODE=production env-cmd node index.js", // 打包文件 "build": "webpack --config webpack.product.config.js", // 打包 dll 文件 "build:dll": "webpack --config webpack.config.dll.js" }}
(完)
]]>单元测试( unit testing ),是指对软件中的最小可测试单元进行检查和验证。在前端领域来说,我们主要是针对 JavaScript 的类( class ) 或者方法( function ) 进行单元测试,以增强代码的可靠性和可维护性。下面介绍的是 mocha 单元测试框架。
mocha 是一个测试框架,可以通过 npm 全局安装在本地,或者是局部安装在项目中。
# 全局安装npm i -g mocha# 局部安装在 dev 环境下npm i --save-dev mocha
在测试文件中,写入如下代码:
function add(n, m) { return n + m;}describe("add", () => { it('should return 2', () => { const count = add(1, 1); if ( count !== 2) { throw new Error("1 + 1 应该等于2"); } });});
运行 mocha
mocha# 或./node_modules/.bin/mocha# 或指定文件./node_modules/.bin/mocha --file ./src/__test__/*.test.js
mocha 命令常用参数
Options: -h, --help 输出帮助信息 -V, --version 输出mocha的版本号 -A, --async-only 强制所有的测试用例必须使用callback或者返回一个promise的格式来确定异步的正确性 -c, --colors 在报告中显示颜色 -C, --no-colors 在报告中禁止显示颜色 -g, --growl 在桌面上显示测试报告的结果 -O, --reporter-options <k=v,k2=v2,...> 设置报告的基本选项 -R, --reporter <name> 指定测试报告的格式 -S, --sort 对测试文件进行排序 -b, --bail 在第一个测试没有通过的时候就停止执行后面所有的测试 -d, --debug 启用node的debugger功能 -g, --grep <pattern> 用于搜索测试用例的名称,然后只执行匹配的测试用例 -f, --fgrep <string> 只执行测试用例的名称中含有string的测试用例 -gc, --expose-gc 展示垃圾回收的log内容 -i, --invert 只运行不符合条件的测试用例,必须和--grep或--fgrep之一同时运行 -r, --require <name> require指定模块 -s, --slow <ms> 指定slow的时间,单位是ms,默认是75ms -t, --timeout <ms> 指定超时时间,单位是ms,默认是200ms -u, --ui <name> 指定user-interface (bdd|tdd|exports)中的一种 -w, --watch 用来监视指定的测试脚本。只要测试脚本有变化,就会自动运行Mocha --check-leaks 检测全局变量造成的内存泄漏问题 --full-trace 展示完整的错误栈信息 --compilers <ext>:<module>,... 使用给定的模块来编译文件 --debug-brk 启用nodejs的debug模式 --es_staging 启用全部staged特性 --harmony<_classes,_generators,...> all node --harmony* flags are available --preserve-symlinks 告知模块加载器在解析和缓存模块的时候,保留模块本身的软链接信息 --icu-data-dir include ICU data --inline-diffs 用内联的方式展示actual/expected之间的不同 --inspect 激活chrome浏览器的控制台 --interfaces 展示所有可用的接口 --no-deprecation 不展示warning信息 --no-exit require a clean shutdown of the event loop: mocha will not call process.exit --no-timeouts 禁用超时功能 --opts <path> 定义option文件路径 --perf-basic-prof 启用linux的分析功能 --prof 打印出统计分析信息 --recursive 包含子目录中的测试用例 --reporters 展示所有可以使用的测试报告的名称 --retries <times> 设置对于失败的测试用例的尝试的次数 --throw-deprecation 无论任何时候使用过时的函数都抛出一个异常 --trace 追踪函数的调用过程 --trace-deprecation 展示追踪错误栈 --use_strict 强制使用严格模式 --watch-extensions <ext>,... --watch监控的扩展 --delay 异步测试用例的延迟时间 --extension 指定测试文件后缀 --file 指定测试文件目录 ...
除了以上命令参数外,可以输入 mocha -h
查看更多命令参数。更多关于 mocha 的使用,请看文档。
为了更友好的显示测试结果,可以使用 chai 断言库:
# 安装 chainpm i --save-dev chai
使用方法:
// 这里是 es5 的使用方法,使用 ES6 在下面会讲到var expect = require("chai").expect;function add(n, m) { return n + m;}describe("add", () => { it("should return 2", () => { const count = add(1, 1); expect(count).to.eqls(2); });});
更多关于 chai 的使用请看文档。
如果我们要在测试文件中写 ES6 语法的话,需要通过 @babel/register
编译
# 安装 babelnpm i @babel/register# 运行时添加 --require./node_modules/.bin/mocha --require @babel/register --file ./src/__test__/*.js
这个时候测试文件就可以写 ES6 的语法了
import { expect } from "chai";import { add } from "./add";describe("add", () => { it("should return 2", () => { const count = add(1, 1); expect(count).to.eqls(2); });});
sinon 提供 stub 和 spy 等函数进行截取和模拟真实函数,以更简单的方式进行测试:
import { expect } from "chai";import { stub } from "sinon";import calc from "./calc";// describe 可以嵌套,通常一个测试只测试某一个文件,现在测试 calc 整个文件describe("calc", () => { // 最好不要这样直接 stub 要测试的方法,不然还测试什么呢? decribe("add", () => { it("should return 2", () => { calcAddStub = stub(calc, "add").returns(2); // 仅仅为了举例 expect(calcAddStub()).to.eqls(2); }); }); // 还可以通过 resolves 模拟异步操作 decribe("calc time", () => { it("should return correct time", async () => { const calcTimeStub = stub(calc, "add").resolves(2); // 仅仅为了举例 const count = await calcTimeStub(); expect(count).to.eqls(2); }); });});
更多关于 sinon 的使用请看文档。
如果要测试 React 组件,可以使用 enzyme 进行模拟
import { expect } from "chai";import { stub } from "sinon";import { shallow } from "enzyme";import App from "./App";describe("App", () => { const defaultProps = { count: 1, }; const render = (props) => shallow(<App {...props} {...defaultProps} />); decribe("add", () => { it("should return 2", () => { const component = render(); const count = component.instance().add(1, 1); expect(count).to.eqls(2); }); });});
更多关于 enzyme 的使用请看文档。
一般项目都需要达到某一覆盖率以上,以确保代码的健壮性,可以使用 Istanbul (伊斯坦布尔) 包。
# 安装,Istanbul 包改名了,叫 nycnpm i --save-dev nyc# 运行./node_modules/.bin/nyc ./node_modules/.bin/mocha --require @babel/register --file ./src/__test__/*.test.js
运行会重新跑一次测试,并且在当前目录生成 .coverage 目录,可以直接在浏览器打开并且查看覆盖率。
更多关于 enzyme 的使用请看文档。
本篇文章只是简单介绍 js 的单元测试流程,使用的技术包括但不限于 mocha (测试框架), chai (断言库), sinon (截取和模拟函数), enzyme (测试 React 等库), Istanbul (查看覆盖率)。在实际应用中,还需要更多的实际操作,例如测试流程和规范,有的项目中需要测试 渲染的 dom 和 dom 中的属性是否正确,有的仅测试方法,当然还有其他的测试框架例如 jest 等,由于 太懒 篇幅有限,点到为止。
(done)
]]>执行上下文是评估和执行 JavaScript 代码的环境的抽象概念,Javascript 代码都是在执行上下文中运行。
执行栈,也叫调用栈,具有 LIFO(后进先出)结构,用于存储在代码执行期间创建的所有执行上下文。
首次运行 JS 代码时,会创建一个全局执行上下文并 Push 到当前的执行栈中。每当发生函数调用,引擎都会为该函数创建一个新的函数执行上下文并 Push 到当前执行栈的栈顶。
根据执行栈 LIFO 规则,当栈顶函数运行完成后,其对应的函数执行上下文将会从执行栈中 Pop 出,上下文控制权将移到当前执行栈的下一个执行上下文。
执行上下文总共有三种类型
执行上下文分两个阶段创建:
1、确定 this 的值,也被称为 This Binding。
2、LexicalEnvironment(词法环境) 组件被创建。
3、VariableEnvironment(变量环境) 组件被创建。
词法环境有两个组成部分
环境记录:存储变量和函数声明的实际位置
对外部环境的引用:可以访问其外部词法环境
词法环境有两种类型
全局环境:是一个没有外部环境的词法环境,其外部环境引用为 null 。拥有一个全局对象( window 对象)及其关联的方法和属性(例如数组方法)以及任何用户自定义的全局变量, this 的值指向这个全局对象。
函数环境:用户在函数中定义的变量被存储在环境记录中,包含了 arguments 对象。对外部环境的引用可以是全局环境,也可以是包含内部函数的外部函数环境。
变量环境也是一个词法环境,因此它具有上面定义的词法环境的所有属性。
在 ES6 中,词法环境和变量环境的区别在于前者用于存储 函数声明和变量( let 和 const )绑定,而后者仅用于存储变量( var ) 绑定。
变量提升的原因:在创建阶段,函数声明存储在环境中,而变量会被设置为 undefined(在 var 的情况下)或保持未初始化(在 let 和 const 的情况下)。所以这就是为什么可以在声明之前访问 var 定义的变量(尽管是 undefined ),但如果在声明之前访问 let 和 const 定义的变量就会提示引用错误的原因。这就是所谓的变量提升。
foo(); // foo2var foo = function () { console.log("foo1");};foo(); // foo1,foo重新赋值function foo() { console.log("foo2");}foo(); // foo1
注意: 函数声明优先级高于变量声明,同一作用域下存在多个同名函数声明,后面的会替换前面的函数声明。
此阶段,完成对所有变量的分配,最后执行代码。
如果 Javascript 引擎在源代码中声明的实际位置找不到 let 变量的值,那么将为其分配 undefined 值。
因为 JS 引擎创建了很多的执行上下文,所以 JS 引擎创建了执行上下文栈(Execution context stack,ECS)来管理执行上下文。
当 JavaScript 初始化的时候会向执行上下文栈压入一个全局执行上下文,我们用 globalContext 表示它,并且只有当整个应用程序结束的时候,执行栈才会被清空,所以程序结束之前, 执行栈最底部永远有个 globalContext 。
观察以下两段代码:
var scope = "global scope";function checkscope() { var scope = "local scope"; function f() { return scope; } return f();}checkscope();
var scope = "global scope";function checkscope() { var scope = "local scope"; function f() { return scope; } return f;}checkscope()();
它们的运行结果是一样的,但执行上下文栈的变化不一样。
第一段代码:
ECStack.push(<checkscope> functionContext);ECStack.push(<f> functionContext);ECStack.pop();ECStack.pop();
第二段代码:
ECStack.push(<checkscope> functionContext);ECStack.pop();ECStack.push(<f> functionContext);ECStack.pop();
上面提到过执行上下文的类型有全局执行上下文和函数执行上下文。
在函数上下文中,用活动对象( activation object, AO )来表示变量对象。
活动对象和变量对象的区别在于
1、变量对象( VO )是规范上或者是 JS 引擎上实现的,并不能在 JS 环境中直接访问。
2、当进入到一个执行上下文后,这个变量对象才会被激活,所以叫活动对象( AO ),这时候活动对象上的各种属性才能被访问。
调用函数时,会为其创建一个 Arguments 对象,并自动初始化局部变量 arguments ,指代该 Arguments 对象。所有作为参数传入的值都会成为 Arguments 对象的数组元素。
执行上下文的代码会分成两个阶段进行处理
很明显,这个时候还没有执行代码,此时的变量对象会包括(如下顺序初始化):
这个阶段会顺序执行代码,修改变量对象的值,执行完成后 AO 如下
(done)
]]>试过各种各样的博客搭建平台,最终还是决定依托 HEXO 搭建,我觉得我的博客不需要有多少多复杂的功能,只需要简单清新一点就好了。
做事总是带点目的性,个人博客于我而言,就像是网络上的一个家,这是属于自己的地盘,在网络上有了归属感,我想写点什么、记录点什么,都是自己决定。
希望能一直纯粹下去。^_^
]]>