Skip to main content

· One min read
Jeffrey

📺️ 对于 iOS 系统上的移动端,你可以通过以下方法禁止视频自动全屏播放:

1️⃣ 在<video>元素上添加playsinline属性,例如:

<video src="video.mp4" playsinline></video>

这将告诉 iOS 设备在内联模式下播放视频,而不会自动全屏。

2️⃣ 在<video>元素上的 JavaScript 代码中添加webkit-playsinline属性,例如:

var video = document.getElementById('myVideo')
video.setAttribute('webkit-playsinline', 'true')

或者直接添加标签中。

<video src="video.mp4" playsinline webkit-playsinline="true"></video>

这也是告诉 iOS 设备在内联模式下播放视频的方式。

请注意,虽然使用上述方法可以禁止视频自动全屏播放,但用户仍然可以手动切换到全屏模式。

· One min read
Jeffrey

示例

scss 实现:将 id content_html 内子元素添加滑动进入动画

#content_html {
margin-top: 20px;
padding: 0 15px;

p,
li {
margin: 15px 0;
font-size: 16px;
line-height: 25px;
}

ul,
li {
list-style: inside;
}

h1,
h2,
h3,
h4,
h5,
h6 {
font-size: 19px;
font-weight: 700;
letter-spacing: -0.025rem;
line-height: 1.8rem;
margin: 20px 0;
outline: none;
}

h2 {
font-size: 18px;
}

h3 {
font-size: 17px;
}

h4,
h5,
h6 {
font-size: 16px;
}

@keyframes slide-enter {
0% {
opacity: 0;
transform: translateY(10px);
}

to {
opacity: 1;
transform: none;
}
}

* {
--stagger: 0;
--delay: 150ms;
--start: 0ms;
animation: slide-enter 1s both 1;
animation-delay: calc(var(--start) + var(--stagger) * var(--delay));
}

* {
counter-increment: enter-count;
--stagger: counter(enter-count);
}

.slide-enter-content > * {
--stagger: 0;
--delay: 150ms;
--start: 0ms;
animation: slide-enter 1s both 1;
animation-delay: calc(var(--start) + var(--stagger) * var(--delay));
}

@for $i from 1 through 30 {
*:nth-child(#{$i}) {
--stagger: #{$i};
}
}
}

参考-antfu

· One min read
Jeffrey

解决报错:nginx: [error] open() "/run/nginx.pid" failed (2: No such file or directory)

重启 Nginx 遇到报错:nginx: [error] open() "/run/nginx.pid" failed (2: No such file or directory) 为什么会报错?nginx 被停止时,nginx.pid 被删除了。reload 命令需要通过 nginx.pid 获取进程号,会去找 nginx.pid ,如果不存在,就报错了。

解决问题方法:

简单粗暴,杀死 nginx 进程,然后再启动 nginx

sudo fuser -k 80/tcp #关闭占用80端口的程序(nginx默认端口80)
cd /etc/init.d
sudo nginx -c /etc/nginx/nginx.conf // 启动nginx

· 3 min read
Jeffrey

要在 VS Code 中使用 SSH 免密连接远程服务器,您需要完成以下步骤:

  1. 确保您的远程服务器已安装并启用了 SSH 服务,并且您具有访问权限。

  2. 在本地计算机上安装并启动 Visual Studio Code 编辑器。

  3. 安装 Remote Development 扩展。您可以通过搜索“Remote Development”来找到并安装该扩展。

  4. 在 VS Code 左侧的“活动栏”中,单击“Remote Explorer”图标。在弹出的菜单中,单击“SSH Targets”,然后单击“Add SSH Host”。

  5. 在弹出的窗口中,输入您的远程服务器的 IP 地址、用户名和密码等信息。注意:如果您的 SSH 认证方式是公钥认证,则应使用私钥文件路径而不是密码。

  6. 单击“Connect”按钮,VS Code 将尝试连接到您的远程服务器。

  7. 如果一切正常,您应该能够看到远程服务器的文件系统,以及您可以在其中打开和编辑文件的选项。

  8. 对于更便捷的访问,您可以将您的 SSH 配置保存在配置文件中,以便下次快速连接。

私钥文件路径

生成密钥

  • 本地终端输入ssh-keygen进行密钥生成
  • 命令运行后,一开始会提示密钥的保存文件名,id_rsa 是私钥文件名,id_rsa.pub 是公钥文件名,这里默认,回车就行。
  • 之后提醒第二个问题,询问是否对私钥进行密码保护,这是进一步做了安全保护。这里设置了密码的话,每次密钥登陆时,你还需要输入密钥密码,也是有点麻烦,我这里就不进行设置了,回车不设。 然后密钥就生成了。

上传公钥

  • 将本地生成的公钥内容全部复制到服务器~/.ssh/authorized_keys文件内即可,如果没有需要收懂创建一个
  • 如文件内已有其他公钥,换行输入

· One min read
Jeffrey

在 Vue3 中,当我们在控制台打印一个 ref 数据时:

const conut = ref(0)
console.log(conut)

控制台会显示RefImpl{_rawValue: 0, _shallow: false, __v_isRef: true, _value: 0}

可以发现,打印的数非常不直观,当然,我们可以选择直接打印 cou 的值,这样就只会输出 8,非常直观,那么有没有办法在打印 count 的时候让输出的信息更友好尾?当然可以,浏览器允许我们编写自定义的 formatter,从而自定义输出形式。在 vs3 的源码中,你可以搜索到名为 initcustonfornatt 为例,我们可以打开 Diwtoals 的设置,然后勾选 Console"→" Enable

刷新浏览器我们就会发现内容变成Ref<0>

· One min read
Jeffrey
  • 进入https://search.censys.io/网站搜索栏输入services.http.response.headers.location: account.jetbrains.com/fls-auth,查询成功会返回一些 IP 地址,点击 ip 进入详情,查看端口 80 且状态码为 302 的 ip,复制 ip 地址 输入到 idea 激活窗口进行激活。

    例如 http:123.123.123.123

  • 如激活失败多尝试几个 ip

· 2 min read
Jeffrey

解决 Node.js mysql 客户端不支持认证协议引发的“ER_NOT_SUPPORTED_AUTH_MODE”问题

报错信息

file:///Users/wjf/Desktop/learn/node/mysql.js:12
if (error) throw error;
^

Error: ER_NOT_SUPPORTED_AUTH_MODE: Client does not support authentication protocol requested by server; consider upgrading MySQL client
at Handshake.Sequence._packetToError (/Users/wjf/Desktop/learn/node/node_modules/.pnpm/[email protected]/node_modules/mysql/lib/protocol/sequences/Sequence.js:47:14)
at Handshake.ErrorPacket (/Users/wjf/Desktop/learn/node/node_modules/.pnpm/[email protected]/node_modules/mysql/lib/protocol/sequences/Handshake.js:123:18)
at Protocol._parsePacket (/Users/wjf/Desktop/learn/node/node_modules/.pnpm/[email protected]/node_modules/mysql/lib/protocol/Protocol.js:291:23)
at Parser._parsePacket (/Users/wjf/Desktop/learn/node/node_modules/.pnpm/[email protected]/node_modules/mysql/lib/protocol/Parser.js:433:10)
at Parser.write (/Users/wjf/Desktop/learn/node/node_modules/.pnpm/[email protected]/node_modules/mysql/lib/protocol/Parser.js:43:10)
at Protocol.write (/Users/wjf/Desktop/learn/node/node_modules/.pnpm/[email protected]/node_modules/mysql/lib/protocol/Protocol.js:38:16)
at Socket.<anonymous> (/Users/wjf/Desktop/learn/node/node_modules/.pnpm/[email protected]/node_modules/mysql/lib/Connection.js:88:28)
at Socket.<anonymous> (/Users/wjf/Desktop/learn/node/node_modules/.pnpm/[email protected]/node_modules/mysql/lib/Connection.js:526:10)
at Socket.emit (node:events:527:28)
at addChunk (node:internal/streams/readable:315:12)
--------------------
at Protocol._enqueue (/Users/wjf/Desktop/learn/node/node_modules/.pnpm/[email protected]/node_modules/mysql/lib/protocol/Protocol.js:144:48)
at Protocol.handshake (/Users/wjf/Desktop/learn/node/node_modules/.pnpm/[email protected]/node_modules/mysql/lib/protocol/Protocol.js:51:23)
at Connection.connect (/Users/wjf/Desktop/learn/node/node_modules/.pnpm/[email protected]/node_modules/mysql/lib/Connection.js:116:18)
at file:///Users/wjf/Desktop/learn/node/mysql.js:9:12
at ModuleJob.run (node:internal/modules/esm/module_job:198:25)
at async Promise.all (index 0)
at async ESMLoader.import (node:internal/modules/esm/loader:385:24)
at async loadESM (node:internal/process/esm_loader:88:5)
at async handleMainPromise (node:internal/modules/run_main:61:12) {
code: 'ER_NOT_SUPPORTED_AUTH_MODE',
errno: 1251,
sqlMessage: 'Client does not support authentication protocol requested by server; consider upgrading MySQL client',
sqlState: '08004',
fatal: true
}

出错原因

  • 导致这个错误的原因是,目前,最新的 mysql 模块并未完全支持 MySQL 8 的“caching_sha2_password”加密方式,而“caching_sha2_password”在 MySQL 8 中是默认的加密方式。因此,下面的方式命令是默认已经使用了“caching_sha2_password”加密方式,该账号、密码无法在 mysql 模块中使用。

解决方法

  • 解决方法是从新修改用户 root 的密码,并指定 mysql 模块能够支持的加密方式:
mysql> ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '123456';

· 5 min read
Jeffrey

事件处理

  • Vue 组件通过 props 和通过调用emit. 在本指南中,我们将了解如何使用该emitted()函数验证事件是否正确发出。

计数器组件

这是一个简单的<Counter>组件。它具有一个按钮,当单击该按钮时,会增加一个内部计数变量并发出其值:

<template>
<button @click="handleClick">Increment</button>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const emit = defineEmits(['increment'])
const count = ref(0)
function handleClick() {
count.value += 1
emit('increment', count)
}
</script>

为了全面测试这个组件,我们应该验证是否发出了increment具有最新count值的事件。

断言发出的事件

为此,我们将依赖该emitted()方法。它返回一个对象,其中包含组件已发出的所有事件,以及它们在数组中的参数。让我们看看它是如何工作的:

it('emits an event when clicked', () => {
const wrapper = mount(Counter)

wrapper.find('button').trigger('click')
wrapper.find('button').trigger('click')

expect(wrapper.emitted()).toHaveProperty('increment')
})

首先要注意的是emitted()返回一个对象,其中每个键都匹配一个发出的事件。在这种情况下,increment

这个测试应该通过。我们确保我们发出了一个具有适当名称的事件。

断言事件的论点

这很好——但我们可以做得更好!我们需要检查我们在emit('increment', count)被调用时是否发出了正确的参数。

我们的下一步是断言事件包含该count值。我们通过将参数传递给emitted().

it('emits an event with count when clicked', () => {
const wrapper = mount(Counter)

wrapper.find('button').trigger('click')
wrapper.find('button').trigger('click')

// `emitted()` accepts an argument. It returns an array with all the
// occurrences of `this.$emit('increment')`.
const incrementEvent = wrapper.emitted('increment')

// We have "clicked" twice, so the array of `increment` should
// have two values.
expect(incrementEvent).toHaveLength(2)

// Assert the result of the first click.
// Notice that the value is an array.
expect(incrementEvent[0]).toEqual([1])

// Then, the result of the second one.
expect(incrementEvent[1]).toEqual([2])
})

让我们回顾一下并分解emitted(). 这些键中的每一个都包含测试期间发出的不同值:

// console.log(wrapper.emitted('increment'))
;[
[1], // first time it is called, `count` is 1
[2], // second time it is called, `count` is 2
]

断言复杂事件

想象一下,现在我们的<Counter>组件需要发出一个带有附加信息的对象。例如,我们需要告诉任何监听@increment事件的父组件count是偶数还是奇数:

修改一下我们的点击事件

function handleClick() {
count.value += 1
emit('increment', {
count: count.value,
isEven: count.value % 2 === 0,
})
}

正如我们之前所做的,我们需要在元素上触发click事件。<button>然后,我们emitted('increment')用来确保发出正确的值。

it('emits an event with count when clicked', () => {
const wrapper = mount(Counter)

wrapper.find('button').trigger('click')
wrapper.find('button').trigger('click')

// We have "clicked" twice, so the array of `increment` should
// have two values.
expect(wrapper.emitted('increment')).toHaveLength(2)

// Then, we can make sure each element of `wrapper.emitted('increment')`
// contains an array with the expected object.
expect(wrapper.emitted('increment')[0]).toEqual([
{
count: 1,
isEven: false,
},
])

expect(wrapper.emitted('increment')[1]).toEqual([
{
count: 2,
isEven: true,
},
])
})

测试诸如对象之类的复杂事件负载与测试诸如数字或字符串之类的简单值没有什么不同。

结论

用于emitted()访问从 Vue 组件发出的事件。 emitted(eventName)返回一个数组,其中每个元素代表一个发出的事件。 参数emitted(eventName)[index]以它们发出的相同顺序存储在数组中

· 7 min read
Jeffrey

入门

  • 本教程源码已上传github

什么是 Vue 测试工具?

  • Vue Test Utils (VTU) 是一组实用功能,旨在简化 Vue.js 组件的测试。它提供了一些方法来以隔离的方式挂载和与 Vue 组件交互。 让我们看一个例子:

什么是 Vitest

  • Vitest 是一个由 Vite 提供支持的极速单元测试框架。
  • 你可以在 为什么是 为什么是 Vitest 中了解有关该项目背后的基本原理的更多信息。

vitest 优点

  • 重复使用 Vite 的配置、转换器、解析器和插件 - 在您的应用程序和测试中保持一致。
  • 拥有预期、快照、覆盖等 - 从 Jest 迁移很简单。
  • 智能文件监听模式,就像是测试的 HMR!
  • 由 esbuild 提供的开箱即用 ESM、TypeScript 和 JSX 支持。
import { mount } from '@vue/test-utils'
import { describe, it, expect } from 'vitest'
// 这是测试组件
const MessageComponent = {
template: '<p>{{ msg }}</p>',
props: ['msg'],
}
// 创建测试用例
describe('displays message', () => {
it('displays message', () => {
// 使用 mount函数 挂载组件
const wrapper = mount(MessageComponent, {
props: {
msg: 'Hello world',
},
})
// 断言组件的呈现文本
expect(wrapper.text()).toContain('Hello world') //可通过
expect(wrapper.text()).toContain('Hello') //不可通过
})
})

测试一个 TodoApp

  • 安装组件
  • 查找元素
  • 填写表格
  • 触发事件

入门

TodoApp 我们的一个简单的组件开始: todo 的简单组件

<template>
<div></div>
</template>

<script setup lang="ts" name="TodoApp">
import { ref } from 'vue'
const todos = ref([
{
id: 1,
text: 'Learn Vue.js 3',
completed: false,
},
])
</script>

第一个测试 - 渲染一个 todo

我们将写出验证来做部分的第一个测试。让我们先看看测试然后讨论:

import { mount } from '@vue/test-utils'
import TodoApp from '../TodoApp/index.vue'
import { describe, it, expect } from 'vitest'
describe('renders a todo', () => {
it('renders properly', () => {
const wrapper = mount(TodoApp)
const todo = wrapper.get('[data-test="todo"]')
expect(todo.text()).toBe('Learn Vue.js 3')
})
})
  • 我们从导入 mountVTU 中渲染组件的主要组件。
  • 运行测试会失败,我们通过属性选择器获取元素 [data-test="todo"]

使测试通过

<template>
<div>
<div v-for="todo in todos" :key="todo.id" data-test="todo">
{{ todo.text }}
</div>
</div>
</template>

这个改变,测试就通过了。恭喜!您编写了第一个组件测试。

添加新的待办事项

我们将能够让用户的下一个功能是让用户创建一个新的待办事项。我们需要输入一个输入的表格,供用户输入一些文本。当用户提交表单时,我们新的任务被渲染。我们来看看测试:

import { mount } from '@vue/test-utils'
import TodoApp from '../TodoApp/index.vue'
import { describe, it, expect } from 'vitest'
describe('renders a todo', () => {
it('creates a todo', () => {
const wrapper = mount(TodoApp)
// 通过属性选择器获取全部元素 长度为1
expect(wrapper.findAll('[data-test="todo"]')).toHaveLength(1)
// 设置input元素value = New todo
wrapper.get('[data-test="new-todo"]').setValue('New todo')
// 触发提交事件
wrapper.get('[data-test="form"]').trigger('submit')
// 再次回去长度应为2
expect(wrapper.findAll('[data-test="todo"]')).toHaveLength(2)
})
})

运行这个程序显然会失败。让我们更新 TodoApp.vue 以拥有<form>并测试我们<input>测试,通过:

<template>
<div>
<div v-for="todo in todos" :key="todo.id" data-test="todo">
{{ todo.text }}
</div>
<form data-test="form" @submit.prevent="createTodo">
<input data-test="new-todo" v-model="newTodo" />
<button type="submit"></button>
</form>
</div>
</template>

<script setup lang="ts" name="TodoApp">
import { ref } from 'vue'
const todos = ref([
{
id: 1,
text: 'Learn Vue.js 3',
completed: false,
},
])
const newTodo = ref('')
function createTodo() {
todos.value.push({
id: 2,
text: newTodo.value,
completed: false,
})
}
</script>

我们使用绑定 v-model 到<input>并@submit 监听提交。createTodotodos

这看起来不错,但运行测试显示错误:

待办事项的数量没有。问题是 vitest 以同步方式执行测试, 并可能增加调用最终函数就结束测试。然而,Vue 会异步更新 DOM。我们需要标记测试 async,调用 await 任何导致 DOM 更改的方法。trigger 是这样的方法,所以是 setValue-我们可以按地预先设置并且设置 await 测试应该是一种简单的工作方式:

import { mount } from '@vue/test-utils'
import TodoApp from '../TodoApp/index.vue'
import { describe, it, expect } from 'vitest'
describe('renders a todo', () => {
// 修改测试用例 添加async await
it('creates a todo', async () => {
const wrapper = mount(TodoApp)
expect(wrapper.findAll('[data-test="todo"]')).toHaveLength(1)

await wrapper.get('[data-test="new-todo"]').setValue('New todo')
await wrapper.get('[data-test="form"]').trigger('submit')

// 异步获取长度
await expect(wrapper.findAll('[data-test="todo"]')).toHaveLength(2)
})
})

完成待办事项

现在可以创建待办事项,让我们可以让用户使用如已完成的待办事项为我们/未完成的项目标记。

import { mount } from '@vue/test-utils'
import TodoApp from '../TodoApp/index.vue'
import { describe, it, expect } from 'vitest'
describe('renders a todo', () => {
// 修改测试用例 添加async await
it('completes a todo', async () => {
const wrapper = mount(TodoApp)

await wrapper.get('[data-test="todo-checkbox"]').setValue(true)

expect(wrapper.get('[data-test="todo"]').classes()).toContain('completed')
})
})

本次测试与前两次类似;我们找到一个元素并以相同的方式与之交互(我们 setValue 再次使用,因为我们正在与 a 交互<input>)。

最后,我们做一个断言。我们将为 completed 完成的 todo 应用一个类——然后我们可以使用它来添加一些样式以直观地指示一个 todo 的状态。

我们可以通过更新 todo 元素<template>以包含<input type="checkbox">和类绑定来通过此测试:

<div
v-for="todo in todos"
:key="todo.id"
data-test="todo"
:class="[todo.completed ? 'completed' : '']"
>
{{ todo.text }}
<input type="checkbox" v-model="todo.completed" data-test="todo-checkbox" />
</div>

恭喜!您编写了第一个组件测试。

· 7 min read
Jeffrey

例子

tip

当浏览器窗口的宽度为 600 像素或更小时,将 body 元素的背景颜色更改为“浅蓝色”:

@media only screen and (max-width: 600px) {
body {
background-color: lightblue;
}
}

定义和使用

@media规则在媒体查询中用于为不同的媒体类型/设备应用不同的样式。

媒体查询可用于检查许多事情,例如:

  • 视口的宽度和高度
  • 设备的宽度和高度
  • 方向(平板电脑/手机是横向还是纵向模式?)
  • 解析度

使用媒体查询是向台式机、笔记本电脑、平板电脑和手机提供定制样式表(响应式网页设计)的一种流行技术。

您还可以使用媒体查询来指定某些样式仅适用于打印文档或屏幕阅读器(媒体类型:打印、屏幕或语音)。

除了媒体类型,还有媒体功能。媒体功能通过允许测试用户代理或显示设备的特定功能,为媒体查询提供更具体的细节。例如,您可以仅将样式应用于大于或小于特定宽度的那些屏幕。

CSS 语法

@media not|only mediatype and (mediafeature and|or|not mediafeature) {
CSS-Code;
}
tip

not , onlyand关键字的含义:

not: not 关键字反转整个媒体查询的含义。

only: only 关键字阻止不支持具有媒体功能的媒体查询的旧浏览器应用指定的样式。 它对现代浏览器没有影响。

and: and 关键字将媒体特征与媒体类型或其他媒体特征结合起来。

它们都是可选的。但是,如果您使用notonly,您还必须指定媒体类型。

您还可以为不同的媒体设置不同的样式表,如下所示:

<link
rel="stylesheet"
media="screen and (min-width: 900px)"
href="widescreen.css"
/>
<link
rel="stylesheet"
media="screen and (max-width: 600px)"
href="smallscreen.css"
/>
....

媒体类型

ValueDescription
all默认。用于所有媒体类型设备
print用于打印机
screen用于电脑屏幕、平板电脑、智能手机等
speech用于大声“阅读”页面的屏幕阅读器

媒体功能

ValueDescription
any-hover是否有任何可用的输入机制允许用户悬停在元素上?(在媒体查询第 4 级中添加)
any-pointer是否有任何可用的输入机制是定点设备,如果是,其准确度如何?(在媒体查询第 4 级中添加)
aspect-ratio视口的宽度和高度之间的比率
color输出设备的每个颜色分量的位数
color-gamut用户代理和输出设备支持的近似颜色范围(添加在媒体查询级别 4 中)
color-index设备可以显示的颜色数
grid设备是网格还是位图
height视口高度
hover主输入机制是否允许用户悬停在元素上?(在媒体查询第 4 级中添加)
inverted-colors浏览器或底层操作系统是否反转颜色?(在媒体查询第 4 级中添加)
light-level当前环境光级别(添加到媒体查询级别 4 中)
max-aspect-ratio显示区域的宽度和高度之间的最大比率
max-color输出设备的每个颜色分量的最大位数
max-color-index设备可以显示的最大颜色数
max-height显示区域的最大高度,如浏览器窗口
max-monochrome单色(灰度)设备上每种“颜色”的最大位数
max-resolution设备的最大分辨率,使用 dpi 或 dpcm
max-width显示区域的最大宽度,如浏览器窗口
min-aspect-ratio显示区域的宽度和高度之间的最小比率
min-color输出设备的每个颜色分量的最小位数
min-color-index设备可以显示的最小颜色数
min-height显示区域的最小高度,如浏览器窗口
min-monochrome单色(灰度)设备上每种“颜色”的最小位数
min-resolution设备的最小分辨率,使用 dpi 或 dpcm
min-width显示区域的最小宽度,如浏览器窗口
monochrome单色(灰度)设备上每种“颜色”的位数
orientation视口的方向(横向或纵向模式)
overflow-block输出设备如何处理沿块轴溢出视口的内容(在媒体查询级别 4 中添加)
overflow-inline是否可以滚动沿内联轴溢出视口的内容(在媒体查询级别 4 中添加)
pointer主要输入机制是定点设备吗?如果是,其准确度如何?(在媒体查询第 4 级中添加)
resolution输出设备的分辨率,使用 dpi 或 dpcm
scan输出设备的扫描过程
scripting脚本(如 JavaScript)可用吗?(在媒体查询第 4 级中添加)
update输出设备修改内容外观的速度有多快(添加在媒体查询第 4 级中)
width视口宽度

· 4 min read
Jeffrey

1 介绍

  • Svelte 是一种构建用户界面的全新方法。传统框架(如 React 和 Vue)在浏览器中完成大部分工作,而 Svelte 将这些工作转移到编译步骤中,在您构建应用程序时发生。
  • Svelte 没有使用虚拟 DOM diffing 之类的技术,而是编写了在应用程序状态发生变化时以手术方式更新 DOM 的代码。
  • 少写代码 | 虚拟 DOM 是纯粹的开销 | 真正的反应
npm create vite@latest myapp -- --template svelte
cd myapp
npm install
npm run dev

2 单文件结构

  • 单页面的创建是 文件名.svelte 后缀名为 svelte, 所以需要学习一些 svelte 语法
  • svelte 页面的结构由三部分组成,script element style 如下代码所示
index.svelte
<!-- 支持ts语法 -->
<script lang="ts">
let msg: string = 'welcome to svelte'
</script>
<div>{msg}</div>
<style></style>

3 语法

3.1 数据渲染

<script lang="ts">
let msg:string = "welcome to svelte"
</script>
<div>
{msg}
</div>

3.2 数据依赖

<script>
let count = 0
$: doubled = count * 2
</script>
<div class="card">
<p>{count}</p>
<p>{count} doubled is {doubled}</p>
</div>

3.3 条件渲染

<script lang="ts">
let msg:string = "welcome to svelte"
let num:number =1
</script>
{#if num == 1}
<h1>{msg}</h1>
{:else if num == 2}
<h1>defualt</h1>
{:else}
<h1>{msg}</h1>
{/if}

3.4 循环渲染

<script lang="ts">
let array:array = [{name: "jack", id: 1}, {name: "tom", id: 2}]
let num:number =1
</script>
// item 迭代每一项 index 下标 (item.id) 更新标识,提高性能
// 支持解构 如{#each array as {id, name}, index}
{#each array as item, index (item.id)}
<h2>{item.name}</h2>
{/each}

3.4 异步 Promise

{#await promise()}
<p>...waiting</p>
{:then number}
<p>The number is {number}</p>
{:catch error}
<p style="color: red">{error.message}</p>
{/await}

3.5 事件绑定

<script>
let count = 0
function incrementCount() {
count += 1
}
</script>
<!-- 添加修饰符 只调用一次 -->
<button on:click|once="{incrementCount}">Clicked {count}</button>
  • 支持修饰符如 once | preventDefault | stopPropagation | passive | nonpassive | self | trusted
  • 支持 on:click|once|capture={...}

3.6 组件部分

  • 组件命名需首字母大写
Card.svelte
<script>
// 接收参数
export let msg: string
// 默认参数 只需对变量赋值
export let msg: string = 'default'
</script>

<div class="card">
<h2>{msg}</h2>
</div>
index.svelte
<script>
// 引入组件
import Card from './Card.svelte'
</script>

<div class="card">
<!-- 使用组件 传递参数 -->
<Card msg="hello card" />
</div>

3.7 表单元素绑定值

  • 切记是 bind:value={变量} 不是 value={变量}
<script>
let name = 'world'
</script>
<input bind:value="{name}" />

<h1>Hello {name}!</h1>

3.8 元素绑定属性

  • bind:innerHTML = {html}
  • bind:clientWidth={w}
  • bind:clientHeight={h}

3.9 生命周期

  • 生命周期函数是从中 svelte 获取例如
import { onMount, afterUpdate } from 'svelte'
  • onMount 组件挂载
  • onDestroy 组件销毁
  • onDestroy 组件销毁
  • beforeUpdate afterUpdate 组件更新前后 参考
  • tick 它返回一个 promise,该 promise 将在任何挂起的状态更改应用于 DOM 后立即进行解析(如果没有挂起状态更改,则立即进行解析)

· 2 min read
Jeffrey

一个基于 ssh2 的纯 javascript 安全拷贝程序。

安装

pnpm install scp2 -g

高级 API

var client = require('scp2')

// 将文件复制到服务器:

client.scp(
'file.txt',
'admin:[email protected]:/home/admin/',
function (err) {}
)

// 将文件复制到服务器,将目标指定为对象:
client.scp(
'file.txt',
{
host: 'example.com',
username: 'admin',
password: 'password',
path: '/home/admin/',
},
function (err) {}
)

// 将文件复制到服务器并重命名:
client.scp(
'file.txt',
'admin:[email protected]:/home/admin/rename.txt',
function (err) {}
)

// 将目录复制到服务器:
client.scp(
'data/',
'admin:[email protected]:/home/admin/data/',
function (err) {}
)

// 从服务器下载文件:
client.scp(
'admin:[email protected]:/home/admin/file.txt',
'./',
function (err) {}
)

// 从服务器下载文件,将目标指定为对象:
client.scp(
{
host: 'example.com',
username: 'admin',
password: 'password',
path: '/home/admin/file.txt',
},
'./',
function (err) {}
)

// 使用私钥登录:
client.scp(
'file.txt',
{
host: 'example.com',
username: 'admin',
privateKey: require('fs').readFileSync('path/to/private/key'),
passphrase: 'private_key_password',
path: '/home/admin/',
},
function (err) {}
)

使用 scp2 实现自动部署前端项目

  • 创建 deploy.js 文件
deploy.js
'use strict'
var client = require('scp2')
let start = +new Date()
const ora = require('ora') //美化控制台
const chalk = require('chalk')
const spinner = ora(chalk.green('正在发布到服务器...'))
spinner.start()
/**
* 输入服务器信息
*/
client.scp(
'dist', // 本地项目路径
{
host: '101.200.0.0',
username: 'root',
password: 123456,
path: '/usr/local/nginx/html',
},
(err) => {
spinner.stop()
if (!err) {
console.log(+new Date() - start + 'ms')
console.log(chalk.green('项目发布完毕!'))
} else {
console.log('发布失败!', err)
}
}
)
  • package.json 添加自动脚本
{
"scripts": {
"deploy": "build && node ./deploy"
}
}
  1. 发送文件之前需要先 build
  2. 终端输入 npm run deploy 即可发布到服务器

· 2 min read
Jeffrey

什么事 Prettier

  • Prettier 是一个固执己见的代码格式化程序,支持:
JavaScript
JSX
Angular
Vue
Flow
TypeScript
CSS, Less, and SCSS
HTML
Ember/Handlebars
JSON
GraphQL
Markdown, including GFM and MDX
YAML
  • 它删除了所有原始样式*并确保所有输出的代码符合一致的样式

安装

npm install --save-dev --save-exact prettier

使用

  • 在 package script 添加 fromat 执行 Prettier
package.json
{
"scripts": {
"format": "prettier --write ."
}
}

运行 npm run format 即可对代码进行格式化

配置文件

  • 创建 .prettierrc |.prettierrc.yml | .prettierrc.yaml 任意文件 已.prettierrc 为例: 基本配置
{
"trailingComma": "es5",
"tabWidth": 4,
"semi": false,
"singleQuote": true
}

忽略文件.prettierignore

  • 要从格式化中排除文件,.prettierignore 请在项目的根目录中创建一个文件。.prettierignore 使用 gitignore 语法。 例如
build
node_modules

*.html

优点

  • 您按保存并格式化代码
  • 无需在代码审查中讨论风格
  • 节省您的时间和精力

· 7 min read
Jeffrey

一、Token

  • 什么是 Token? Token 指访问资源的凭据,是一种身份认证的方式,它是解决跨域认证的最流行的一种方式。

  • 为什么用 Token? 以前较为流行的是通过 session 去做身份认证,session 是通过服务器中保存会话数据来做身份认证,这种方式会导致在高并发中服务器压力过大的情况,还有就是,如果是服务器集群,那么就需要这些服务器 session 共享。

Token 不在服务器中保存会话数据,而是保存在客户端。每次请求的 headers 中存入 Token,在服务器中判断 Token 的有效性,是否可以访问资源。

  • 传统 Token 和 JWT 的区别

    • 传统 Token 用户发起登录请求,登录成功之后返回 Token,并且存于数据库,用户访问资源的时候需要携带 Token,服务端获取 Token 之后和数据库中的对比。
    • JWT 用户发起登录请求,登录成功之后返回 Token,但是不存于数据库,用户访问资源的时候需要携带 Token,服务端获取 Token 之后去校验 Token 的合法性。

二、JWT 实现过程

  • JWT 分为三个部分 header、payload、verify signature

  • header

内部包含有签名算法、Token 类型,然后通过 base64url 算法转成字符串

//明文例子:
{
"alg":"HS256",
"typ":"JWT"
}
  • payload 内部包含 JWT 标准数据和自定义数据,然后通过 base64url 算法转成字符串 JWT 标准数据常见的有:
iss:提供方。
sub:主题,一般是用户ID。
exp:过期时间。
iat:创建时间。
jti:token的唯一标识。

可选择性使用以上标准数据

//明文例子:
{
"id": 3,
"name": "Bmongo",
"age": 18,
"iat": 1588139323,
"exp": 1588139333
}

注意:由于 JWT 是默认不加密的,所以在这边不要存敏感信息

  • verify signature 这部分是对前两部分的签名,防止数据的篡改 secret 是服务器端保存的密钥,只有服务器端知道,再使用 header 中所指定的签名算法对上面的俩部分进行签名,按照以下公式生成签名
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)

复制代码 算出签名之后,把三部分通过.分隔开返回给用户就行了

  • 客户端请求

  • 每次客户端的请求都需要带上这个 token,一般是把 token 写入到请求的 headers 中

三、Node.js 中使用

Node.js 中使用 JWT

1.开始使用

通过 npm 包 jsonwebtoken 来完成 token 的生成和验证

npm install --save jsonwebtoken

2.生成、验证 Token

auth/token.js
const jwt = require('jsonwebtoken')
//撒盐,加密时候混淆
const secret = '113Bmongojsdalkfnxcvmas'

//生成token
//info也就是payload是需要存入token的信息
function createToken(info) {
let token = jwt.sign(info, secret, {
//Token有效时间 单位s
expiresIn: 60 * 60 * 10,
})
return token
}

//验证Token
function verifyToken(token) {
return new Promise((resolve, reject) => {
jwt.verify(token, secret, (error, result) => {
if (error) {
reject(error)
} else {
resolve(result)
}
})
})
}
const whiteList = ['/api/login']

function verifyTokenMiddle(req, res, next) {
if (!whiteList.includes(req.url)) {
// 发起请求是headers.authorization 添加token
verifyToken(req.headers.authorization)
.then((res) => {
next()
})
.catch((e) => {
res.status(401).send('invalid token')
})
} else {
next()
}
}

module.exports = {
verifyTokenMiddle,
createToken,
}

3.使用

app.js
var createError = require('http-errors')var express = require('express')var path = require('path')var cookieParser = require('cookie-parser')var logger = require('morgan')const { verifyTokenMiddle } = require('./auth/token')const expressSession = require('express-session')const redis = require('redis')const { redisConfig } = require('./config/config')// 创建Redis连接配置const redisClient = redis.createClient(redisConfig)const RedisStore = require('connect-redis')(expressSession)redisClient.on('connect', function () {  console.log('Redis client connected')})redisClient.on('error', function (e) {  console.error(e)})var app = express()app.use(express.static(path.join(__dirname, '/public')))//设置跨域访问 -- 开始 --app.all('*', function (req, res, next) {  res.header('Access-Control-Allow-Origin', '*') //的允许所有域名的端口请求(跨域解决)  res.header('Access-Control-Allow-Headers', 'Content-Type')  res.header('Access-Control-Allow-Methods', '*')  res.header('Content-Type', 'application/json;charset=utf-8')  if (req.method === 'OPTIONS') {    res.end()  } else {    next()  }})app.use(cookieParser())app.use(  expressSession({    store: new RedisStore({ client: redisClient }),    name: 'session_id', // 默认connect.sid    secret: 'yupi996', // 设置签名秘钥  内容可以任意填写    resave: true, // 强制保存,如果session没有被修改也要重新保存,默认true(推荐false)    saveUninitialized: true, // 如果原先没有session那么就设置,否则不设置(推荐true)    rolling: true, // 每次请求更新有效时长    cookie: {      domain: '.mianshiya.com',      // 全局设置cookie,就是访问随便api就会设置cookie,也可以在登录的路由下单独设置      maxAge: 1000 * 60 * 60 * 24 * 15, // 15 天后过期      httpOnly: true, // 是否允许客户端修改cookie,(默认true 不能被修改)    },  }))// app.use(logger('dev'));app.use(express.json())app.use(express.urlencoded({ extended: false }))// 添加验证token中间件app.use(verifyTokenMiddle)// 注册路由setRoutes()app.use(function (req, res, next) {  next(createError(404))})// error handlerapp.use(function (err, req, res, next) {  console.log(2)  // set locals, only providing error in development  res.locals.message = err.message  res.locals.error = req.app.get('env') === 'development' ? err : {}  // render the error page  res.status(err.status || 500)  res.send('error')})/** *  注册路由 */function setRoutes() {  const API = '/api/'  const fileDir = 'routes'  const noIncludePath = ['index']  const getPrefix = (path) => path.split('.')[0]  require('fs')    .readdirSync(path.join(__dirname, fileDir))    .filter((path) => noIncludePath.some((noPath) => !path.startsWith(noPath)))    .forEach((api) => {      app.use(        API + getPrefix(api),        require(path.join(__dirname, fileDir + '/') + getPrefix(api))      )    })}module.exports = app
routers/login
var express = require('express')
var router = express.Router()
const { createToken } = require('../auth/token')
router.get('/', function (req, res) {
var user = {
name: 'zs',
ps: 123,
}
let token = createToken(user)
res.send(token)
})
module.exports = router

总结

tip
  • 登录成功之后 服务端根据用户信息生成 token
  • 非登录接口发起请求是需设置 headers.authorization = token
  • 添加token验证中间件,除登录接口 其他接口全部验证 token
  • 验证成功通过,不成功提示错误 :::

· 2 min read
Jeffrey

快速入门

使用  yarn  安装 Jest:

yarn add --dev jest

或使用  npm  安装:

npm install --save-dev jest

注:Jest 的文档统一使用  yarn  指令,但使用  npm  同样可行。可以通过 yarn 官方文档进行  yarn  和  npm  的对比。

下面我们开始给一个假定的函数写测试,这个函数的功能是两数相加。首先创建  sum.js  文件:

function sum(a, b) {
return a + b
}
module.exports = sum

接下来,创建名为  sum.test.js  的文件。这个文件包含了实际测试内容:

const sum = require('./sum')

test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3)
})

将如下代码添加到  package.json  中:

{
"scripts": {
"test": "jest"
}
}

最后,运行  yarn test  或者  npm run test ,Jest 将输出如下信息:

PASS  ./sum.test.js
✓ adds 1 + 2 to equal 3 (5ms)

· One min read
Jeffrey
  • linux 的软链接 linux 的软链接称为符号链接,不受文件系统限制。软链接相当于 win 下的快捷方式,但作用比快捷方式强大。

命令为:

ln -s file filesoft

filesoft 为要建立的软链接路径

  • 删除软链接
rm -fr filesoft

· 2 min read
Jeffrey
  • 如果未安装 gcc 和 gcc-c++可能需要先安装
yum -y install gcc
yum -y install gcc-c++

一、下载

二、解压

将下载好的nginx-x.x.x.tar.gz包上传指服务器

tar -zxvf nginx-x.x.x.tar.gz 解压安装包

三、执行配置文件

解压成功后 依次执行一下命令

1、cd nginx-x.x.x    #进入nginx目录
2、./configure #执行配置文件

执行自动配置报错,具体错误信息去下:

./configure: error: the HTTP rewrite module requires the PCRE library. You can either disable the module by using --without-http_rewrite_module option, or install the PCRE library into the system, or build the PCRE library statically from the source with nginx by using --with-pcre=path option.

需要安装 PCRE,具体命令如下:

yum -y install pcre-devel openssl openssl-devel

3、make #手动安装
4、make install #若不确定再执行次文件
whereis nginx #查看安装目录/usr/local/nginx
cd /usr/local/nginx/sbin # 进入sbin文件执行

5、./nginx #启动nginx 访问ip:80/

四、Nginx 常用命令

./nginx #启动
./nginx -s stop #停止
./nginx -s quit #安全退出
./nginx -s reload #重新加载配置文件
ps -ef|grep nginx #查看Nginx进程

· One min read
Jeffrey

直接部署

  1. 将程序所需要的文件如配置文件和生成的可执行文件拷贝到 linux 中

  2. 直接执行./main 命令,启动程序 (main 是 go 编译生成的可执行文件)

chmod -R 755 main

在后台启动程序

./main 这种启动方法在控制台退出时程序会停止,我们可以用 nohup ./main &命令让程序在后台运行

nohup ./main &

如果需要记录日志的话,可以这样使用

nohup ./main > logs/app.log 2>&1 &

nohup 需要运行的命令 >日志文件路径 2>&1 &

查看程序是否正常运行

ps aux | grep main

杀掉进程

ps -ef|grep "./main"|grep -v grep|awk '{print $2}'|xargs kill -9

· 3 min read
Jeffrey

n– 交互式管理您的 Node.js 版本

  • Node.js 版本管理:没有 subshel​​ls,没有配置文件设置,没有复杂的 API,只是简单的.

支持的平台

tip
  • n 在 macOS、Linux 上受支持,包括适用于 Linux 的 Windows 子系统和各种其他类 unix 系统。它是作为 BASH 脚本编写的,但不需要您使用 BASH 作为命令 shell。

  • n 不适用于 Microsoft Windows(如 PowerShell)或 Git for Windows BASH 或 Cygwin DLL 上的本机 shell。 :::

安装

  • 如果你已经安装了 Node.js,一个简单的安装方法 n 是使用 npm:
npm install -g n

第三方安装程序

brew install n
port install n
curl -L https://bit.ly/n-install | bash

安装 Node.js 版本

  • 只需执行 n version下载并安装一个版本的 Node.js。如果version已经下载,n 将从其缓存中安装。
n 10.16.0
  • n 自行执行以查看您下载的版本,并安装选定的版本。
$ n
node/4.9.1
ο node/8.11.3
node/10.15.0

指定 Node.js 版本

有多种方法可以为 n 命令指定目标 Node.js 版本。大多数命令使用最新的匹配版本,并 n ls-remote 列出多个匹配版本。 数字版本号可以是完整的或不完整的,带有可选的前导 v。

  • 4.9.1

  • 8: 8.xy 版本

  • v6.1: 6.1.x 版本 有两个特别有用的版本的标签:

  • lts: 最新的长期支持官方版本

  • latest, current: 最新官方发布

删除版本

删除一些缓存版本:

n rm 0.9.4 v0.10.0

删除除已安装版本之外的所有缓存版本:

n prune

保留 npm

Node.js 安装通常还包括 npm、 npx 和 corepack,但您可能希望使用以下方式保留当前(尤其是较新)版本--preserve:

$ npm install -g npm@latest
...
$ npm --version
6.13.7
# Node.js 8.17.0 includes (older) npm 6.13.4
$ n -p 8
installed : v8.17.0
$ npm --version
6.13.7

各种各样的

命令行帮助可以从n --help

· 2 min read
Jeffrey
  • PM2 是具有内置负载均衡器的 Node.js 应用程序的生产流程管理器。它允许您使应用程序永远保持活动状态,在不停机的情况下重新加载它们,并促进常见的系统管理任务。

1、pm2 需要全局安装

npm install -g pm2

2、进入项目根目录

2.1 启动进程/应用

pm2 start bin/www 或 pm2 start app.js

2.2 重命名进程/应用

pm2 start app.js --name wb123

2.3 添加进程/应用 watch

pm2 start bin/www --watch

2.4 结束进程/应用

pm2 stop www

2.5 结束所有进程/应用

pm2 stop all

2.6 删除进程/应用

pm2 delete www

2.7 删除所有进程/应用

pm2 delete all

2.8 列出所有进程/应用

pm2 list

2.9 查看某个进程/应用具体情况

pm2 describe www

2.10 查看进程/应用的资源消耗情况

pm2 monit

2.11 查看 pm2 的日志

pm2 logs

2.11.1 删除日志

pm2 flush

2.12 若要查看某个进程/应用的日志,使用

pm2 logs www

2.13 重新启动进程/应用

pm2 restart www

2.14 重新启动所有进程/应用

pm2 restart all

分布式部署(负载均衡)

1、增加多少工作线程

pm2 scale app +3

2、减少多少工作线程

· One min read
Jeffrey

Docusaurus blogging features are powered by the blog plugin.

Simply add Markdown files (or folders) to the blog directory.

Regular blog authors can be added to authors.yml.

The blog post date can be extracted from filenames, such as:

  • 2019-05-30-welcome.md
  • 2019-05-30-welcome/index.md

A blog post folder can be convenient to co-locate blog post images:

Docusaurus Plushie

The blog supports tags as well!

And if you don't want a blog: just delete this directory, and use blog: false in your Docusaurus config.