前言
文章首次发表在 个人博客
之前写过一篇 web安全之XSS实例解析,是通过举的几个简单例子讲解的,同样通过简单得例子来理解和学习CSRF,有小伙伴问实际开发中有没有遇到过XSS和CSRF,答案是有遇到过,不过被测试同学发现了,还有安全扫描发现了可能的问题,这两篇文章就是简化了一下当时实际遇到的问题。
CSRF
跨站请求伪造(Cross Site Request Forgery),是指黑客诱导用户打开黑客的网站,在黑客的网站中,利用用户的登陆状态发起的跨站请求。CSRF攻击就是利用了用户的登陆状态,并通过第三方的站点来做一个坏事。
要完成一次CSRF攻击,受害者依次完成两个步骤:
登录受信任网站A,并在本地生成Cookie
在不登出A的情况,访问危险网站B
CSRF攻击
在a.com登陆后种下cookie, 然后有个支付的页面,支付页面有个诱导点击的按钮或者图片,第三方网站域名为 b.com,中的页面请求 a.com的接口,b.com 其实拿不到cookie,请求 a.com会把Cookie自动带上(因为Cookie种在 a.com域下)。这就是为什么在服务端要判断请求的来源,及限制跨域(只允许信任的域名访问),然后除了这些还有一些方法来防止 CSRF 攻击,下面会通过几个简单的例子来详细介绍 CSRF 攻击的表现及如何防御。
下面会通过一个例子来讲解 CSRF 攻击的表现是什么样子的。
实现的例子:
在前后端同域的情况下,前后端的域名都为 http://127.0.0.1:3200, 第三方网站的域名为 http://127.0.0.1:3100,钓鱼网站页面为 http://127.0.0.1:3100/bad.html。
平时自己写例子中会用到下面这两个工具,非常方便好用:
http-server: 是基于node.js的HTTP 服务器,它最大的好处就是:可以使用任意一个目录成为服务器的目录,完全抛开后端的沉重工程,直接运行想要的js代码;
nodemon: nodemon是一种工具,通过在检测到目录中的文件更改时自动重新启动节点应用程序来帮助开发基于node.js的应用程序
前端页面: client.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>CSRF-demo</title>
<style>
.wrap {
height: 500px;
width: 300px;
border: 1px solid #ccc;
padding: 20px;
margin-bottom: 20px;
}
input {
width: 300px;
}
.payInfo {
display: none;
}
.money {
font-size: 16px;
}
</style>
</head>
<body>
<div class="wrap">
<div class="loginInfo">
<h3>登陆</h3>
<input type="text" placeholder="用户名" class="userName">
<br>
<input type="password" placeholder="密码" class="password">
<br>
<br>
<button class="btn">登陆</button>
</div>
<div class="payInfo">
<h3>转账信息</h3>
<p >当前账户余额为 <span class="money">0</span>元</p>
<!-- <input type="text" placeholder="收款方" class="account"> -->
<button class="pay">支付10元</button>
<br>
<br>
<a href="http://127.0.0.1:3100/bad.html" target="_blank">
听说点击这个链接的人都赚大钱了,你还不来看一下么
</a>
</div>
</div>
</body>
<script>
const btn = document.querySelector('.btn');
const loginInfo = document.querySelector('.loginInfo');
const payInfo = document.querySelector('.payInfo');
const money = document.querySelector('.money');
let currentName = '';
// 第一次进入判断是否已经登陆
Fetch('http://127.0.0.1:3200/isLogin', 'POST', {})
.then((res) => {
if(res.data) {
payInfo.style.display = "block"
loginInfo.style.display = 'none';
Fetch('http://127.0.0.1:3200/pay', 'POST', {userName: currentName, money: 0})
.then((res) => {
money.innerHTML = res.data.money;
})
} else {
payInfo.style.display = "none"
loginInfo.style.display = 'block';
}
})
// 点击登陆
btn.onclick = function () {
var userName = document.querySelector('.userName').value;
currentName = userName;
var password = document.querySelector('.password').value;
Fetch('http://127.0.0.1:3200/login', 'POST', {userName, password})
.then((res) => {
payInfo.style.display = "block";
loginInfo.style.display = 'none';
money.innerHTML = res.data.money;
})
}
// 点击支付10元
const pay = document.querySelector('.pay');
pay.onclick = function () {
Fetch('http://127.0.0.1:3200/pay', 'POST', {userName: currentName, money: 10})
.then((res) => {
console.log(res);
money.innerHTML = res.data.money;
})
}
// 封装的请求方法
function Fetch(url, method = 'POST', data) {
return new Promise((resolve, reject) => {
let options = {};
if (method !== 'GET') {
options = {
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
}
}
fetch(url, {
mode: 'cors', // no-cors, cors, *same-origin
method,
...options,
credentials: 'include',
}).then((res) => {
return res.json();
}).then(res => {
resolve(res);
}).catch(err => {
reject(err);
});
})
}
</script>
</html>
实现一个简单的支付功能:
会首先判断有没有登录,如果已经登陆过,就直接展示转账信息,未登录,展示登陆信息
登陆完成之后,会展示转账信息,点击支付,可以实现金额的扣减
后端服务: server.js
const Koa = require("koa");
const app = new Koa();
const route = require('koa-route');
const bodyParser = require('koa-bodyparser');
const cors = require('@koa/cors');
const KoaStatic = require('koa-static');
let currentUserName = '';
// 使用 koa-static 使得前后端都在同一个服务下
app.use(KoaStatic(__dirname));
app.use(bodyParser()); // 处理post请求的参数
// 初始金额为 1000
let money = 1000;
// 调用登陆的接口
const login = ctx => {
const req = ctx.request.body;
const userName = req.userName;
currentUserName = userName;
// 简单设置一个cookie
ctx.cookies.set(
'name',
userName,
{
domain: '127.0.0.1', // 写cookie所在的域名
path: '/', // 写cookie所在的路径
maxAge: 10 * 60 * 1000, // cookie有效时长
expires: new Date('2021-02-15'), // cookie失效时间
overwrite: false, // 是否允许重写
SameSite: 'None',
}
)
ctx.response.body = {
data: {
money,
},
msg: '登陆成功'
};
}
// 调用支付的接口
const pay = ctx => {
if(ctx.method === 'GET') {
money = money - Number(ctx.request.query.money);
} else {
money = money - Number(ctx.request.body.money);
}
ctx.set('Access-Control-Allow-Credentials', 'true');
// 根据有没有 cookie 来简单判断是否登录
if(ctx.cookies.get('name')){
ctx.response.body = {
data: {
money: money,
},
msg: '支付成功'
};
}else{
ctx.body = '未登录';
}
}
// 判断是否登陆
const isLogin = ctx => {
ctx.set('Access-Control-Allow-Credentials', 'true');
if(ctx.cookies.get('name')){
ctx.response.body = {
data: true,
msg: '登陆成功'
};
}else{
ctx.response.body = {
data: false,
msg: '未登录'
};
}
}
// 处理 options 请求
app.use((ctx, next)=> {
const headers = ctx.request.headers;
if(ctx.method === 'OPTIONS') {
ctx.set('Access-Control-Allow-Origin', headers.origin);
ctx.set('Access-Control-Allow-Headers', 'Content-Type');
ctx.set('Access-Control-Allow-Credentials', 'true');
ctx.status = 204;
} else {
next();
}
})
app.use(cors());
app.use(route.post('/login', login));
app.use(route.post('/pay', pay));
app.use(route.get('/pay', pay));
app.use(route.post('/isLogin', isLogin));
app.listen(3200, () => {
console.log('启动成功');
});
执行 nodemon server.js,访问页面 http://127.0.0.1:3200/client.html
CSRF-demo
登陆完成之后,可以看到Cookie是种到 http://127.0.0.1:3200 这个域下面的。
第三方页面 bad.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>第三方网站</title>
</head>
<body>
<div>
哈哈,小样儿,哪有赚大钱的方法,还是踏实努力工作吧!
<!-- form 表单的提交会伴随着跳转到action中指定 的url 链接,为了阻止这一行为,可以通过设置一个隐藏的iframe 页面,并将form 的target 属性指向这个iframe,当前页面iframe则不会刷新页面 -->
<form action="http://127.0.0.1:3200/pay" method="POST" class="form" target="targetIfr" style="display: none">
<input type="text" name="userName" value="xiaoming">
<input type="text" name="money" value="100">
</form>
<iframe name="targetIfr" style="display:none"></iframe>
</div>
</body>
<script>
document.querySelector('.form').submit();
</script>
</html>
使用 HTTP-server 起一个 本地端口为 3100的服务,就可以通过 http://127.0.0.1:3100/bad.html 这个链接来访问,CSRF攻击需要做的就是在正常的页面上诱导用户点击链接进入这个页面
CSRF-DEMO
点击诱导链接,跳转到第三方的页面,第三方页面自动发了一个扣款的请求,所以在回到正常页面的时候,刷新,发现钱变少了。
我们可以看到在第三方页面调用 http://127.0.0.1:3200/pay 这个接口的时候,Cookie自动加在了请求头上,这就是为什么 http://127.0.0.1:3100/bad.html 这个页面拿不到 Cookie,但是却能正常请求 http://127.0.0.1:3200/pay 这个接口的原因。
CSRF攻击大致可以分为三种情况,自动发起Get请求, 自动发起POST请求,引导用户点击链接。下面会分别对上面例子进行简单的改造来说明这三种情况
自动发起Get请求
在上面的 bad.html中,我们把代码改成下面这样
<!DOCTYPE html>
<html>
<body>
<img src="http://127.0.0.1:3200/payMoney?money=1000">
</body>
</html>
当用户访问含有这个img的页面后,浏览器会自动向自动发起 img 的资源请求,如果服务器没有对该请求做判断的话,那么会认为这是一个正常的链接。
自动发起POST请求
上面例子中演示的就是这种情况。
<body>
<div>
哈哈,小样儿,哪有赚大钱的方法,还是踏实努力工作吧!
<!-- form 表单的提交会伴随着跳转到action中指定 的url 链接,为了阻止这一行为,可以通过设置一个隐藏的iframe 页面,并将form 的target 属性指向这个iframe,当前页面iframe则不会刷新页面 -->
<form action="http://127.0.0.1:3200/pay" method="POST" class="form" target="targetIfr">
<input type="text" name="userName" value="xiaoming">
<input type="text" name="money" value="100">
</form>
<iframe name="targetIfr" style="display:none"></iframe>
</div>
</body>
<script>
document.querySelector('.form').submit();
</script>
上面这段代码中构建了一个隐藏的表单,表单的内容就是自动发起支付的接口请求。当用户打开该页面时,这个表单会被自动执行提交。当表单被提交之后,服务器就会执行转账操作。因此使用构建自动提交表单这种方式,就可以自动实现跨站点 POST 数据提交。
引导用户点击链接
诱惑用户点击链接跳转到黑客自己的网站,示例代码如图所示
<a href="http://127.0.0.1:3100/bad.html">听说点击这个链接的人都赚大钱了,你还不来看一下么</a>
用户点击这个地址就会跳到黑客的网站,黑客的网站可能会自动发送一些请求,比如上面提到的自动发起Get或Post请求。
如何防御CSRF
利用cookie的SameSite
SameSite有3个值: Strict, Lax和None
Strict。浏览器会完全禁止第三方cookie。比如a.com的页面中访问 b.com 的资源,那么a.com中的cookie不会被发送到 b.com服务器,只有从b.com的站点去请求b.com的资源,才会带上这些Cookie
Lax。相对宽松一些,在跨站点的情况下,从第三方站点链接打开和从第三方站点提交 Get方式的表单这两种方式都会携带Cookie。但如果在第三方站点中使用POST方法或者通过 img、Iframe等标签加载的URL,这些场景都不会携带Cookie。
None。任何情况下都会发送 Cookie数据
我们可以根据实际情况将一些关键的Cookie设置 Stirct或者 Lax模式,这样在跨站点请求的时候,这些关键的Cookie就不会被发送到服务器,从而使得CSRF攻击失败。
验证请求的来源点
由于CSRF攻击大多来自第三方站点,可以在服务器端验证请求来源的站点,禁止第三方站点的请求。
可以通过HTTP请求头中的 Referer和Origin属性。
HTTP请求头
但是这种 Referer和Origin属性是可以被伪造的,碰上黑客高手,这种判断就是不安全的了。
CSRF Token
最开始浏览器向服务器发起请求时,服务器生成一个CSRF Token。CSRF Token其实就是服务器生成的字符串,然后将该字符串种植到返回的页面中(可以通过Cookie)
浏览器之后再发起请求的时候,需要带上页面中的 CSRF Token(在request中要带上之前获取到的Token,比如 x-csrf-token:xxxx), 然后服务器会验证该Token是否合法。第三方网站发出去的请求是无法获取到 CSRF Token的值的。
其他知识点补充
1. 第三方cookie
Cookie是种在服务端的域名下的,比如客户端域名是 a.com,服务端的域名是 b.com, Cookie是种在 b.com域名下的,在 Chrome的 Application下是看到的是 a.com下面的Cookie,是没有的,之后,在a.com下发送b.com的接口请求会自动带上Cookie(因为Cookie是种在b.com下的)
2. 简单请求和复杂请求
复杂请求需要处理option请求。
之前写过一篇特别详细的文章 CORS原理及@koa/cors源码解析,有空可以看一下。
3. Fetch的 credentials 参数
如果没有配置credential 这个参数,fetch是不会发送Cookie的
credential的参数如下
include:不论是不是跨域的请求,总是发送请求资源域在本地的Cookies、HTTP Basic anthentication等验证信息
same-origin:只有当URL与响应脚本同源才发送 cookies、 HTTP Basic authentication 等验证信息
omit: 从不发送cookies.
平常写一些简单的例子,从很多细节问题上也能补充自己的一些知识盲点。
做项目的时候准备把js项目重构成ts项目,需要把文件后缀改成ts,一个bat脚本搞定,命令如下:
@echo off
rem 正在搜索...
for /f "delims=" %%i in ('dir /b /a-d /s "*.js"') do ren "%%i" "%%~ni.ts" rem 搜索完毕 @pause
把脚本放到根目录下,双击运行完就可以了
网上可以找到前端开发社区贡献的大量工具,这篇文章列出了我最喜欢的一些工具,这些工具给我的工作带来了许多便利。
1. EnjoyCSS
老实说,虽然我做过许多前端开发,但我并不擅长 CSS。当我陷入困境时,EnjoyCSS 是我的大救星。EnjoyCSS 提供了一个简单的交互界面,帮助我设计元素,然后自动输出相应的 CSS 代码。
EnjoyCSS 可以输出 CSS、LESS、SCSS 代码,并支持指定需要支持哪些浏览器及其版本。开发简单页面时用起来比较方便,但不太适合复杂一点的前端项目(这类项目往往需要引入 CSS 框架)。
2. Prettier Playground
Prettier 是一个代码格式化工具,支持格式化 JavaScript 代码(包括 ES2017、JSX、Angular、Vue、Flow、TypeScript 等)。Prettier 会移除代码原本的样式,替换为遵循最佳实践的标准化、一致的样式。IDE 大多支持 Prettier 工具,不过 Prettier 也有在线版本,让你可以在浏览器里格式化代码。
如果工作电脑不在手边,使用移动端设备或者临时借用别人的电脑查看代码时,Prettier Playground 非常好用。相比在 IDE 或编辑器下使用 Prettier,个人更推荐通过 git pre-commit hook 配置 Prettier:hook 可以保证整个团队使用统一的配置,免去各自分别配置 IDE 或编辑器的麻烦。如果是老项目,hook 还可以设置只格式化有改动的单个文件甚至有改动的代码段,避免在 IDE 或编辑器下使用 Prettier 时不小心格式了大量代码,淹没了 commit 的主要改动,让 review 代码变得十分痛苦。
3. Postman
Postman 一直在我的开发工具箱里,测试后端 API 接口时非常好用。GET、POST、DELETE、OPTIONS、PUT 这些方法都支持。毫无疑问,你应该使用这个工具。
Postman 之外,Insomnia 也是很流行的 REST API 测试工具,亮点是支持 GraphQL。不过 Postman 从 去年夏天发布的 v7.2 起也支持了 GraphQL。
4. StackBlitz
Chidume Nnamdi 盛赞这是每个用户最喜欢的在线 IDE。StackBlitz 将大家最喜欢、最常用的 IDE Visual Studio Code 搬进了浏览器。
StackBlitz 支持一键配置 Angular、React、Ionic、TypeScript、RxJS、Svelte 等 JavaScript 框架,也就是说,只需几秒你就可以开始写代码了。
我觉得这个在线 IDE 很有用,特别是可以在线尝试一些样例代码或者库,否则仅仅尝试一些新特性就需要花很多时间在新项目初始化配置上。有了 StackBlitz,无需在本地从头搭建环境,花上几分钟就可以试用一个 NPM 包。很棒,不是吗?
微软官方其实也提供了在线版本的 VSCode,可以在浏览器内使用 VSCode,并且支持开发 Node.js 项目(基于 Azure)。不过 StackBlitz 更专注于优化前端开发体验,界面更加直观一点,也推出了 beta 版本的 Node.js 支持(基于 GCP,需要填表申请)。
5. Bit.dev
软件开发的基本原则之一就是代码复用。代码复用减少了开发量,让你不用从头开发组件。
这正是 Bit.dev 做的事,分享可重用的组件和片段,降低开发量,加速开发进程。
除了公开分享,它还支持在团队分享,让团队协作更方便。
正如 Bit.dev 的口号「组件即设计体系。协同开发更好的组件。」所言,Bit.dev 可以用来创建设计体系,允许团队内的开发者和设计师一起协作,从头搭建一套设计体系。
Bit.dev 目前支持 React、Vue、Angular、Node 及其他 JavaScript 框架。
在 Bit.dev 上不仅可以搜索组件,还可以直接查看组件的依赖,浏览组件的代码,甚至在线编辑代码并查看预览效果!选好组件后可以通过 Bit.dev 的命令行工具 bit 在本地项目引入组件,也可以通过 npm、yarn 引入组件。
6. CanIUse
CanIUse是非常好用的在线工具,可以方便地查看各大浏览器对某个特性的支持程度。
我过去经常碰到自己开发的应用的一些功能在其他浏览器下不支持的情况。比如我的作品集项目使用的某个特性在 Safari 下不支持,直到项目上线几个月后我才意识到。这些经验教训让我意识到需要检查浏览器兼容性。
我们来看一个例子吧。哪些浏览器支持 WebP 图像格式?
如你所见,Safari 和 IE 目前不支持 WebP。这意味着需要为不兼容的浏览器提供回退选项,比如:
<picture>
CanIUse 还可以在命令行下使用,例如,在命令行下查看 WebP 图像格式的浏览器兼容性:caniuse webp(运行命令前需要事先通过 npm install -g caniuse-cmd安装命令行工具。
方法参数的验证
JavaScript 允许你设置参数的默认值。通过这种方法,可以通过一个巧妙的技巧来验证你的方法参数。
const isRequired = () => { throw new Error('param is required'); };
const print = (num = isRequired()) => { console.log(`printing ${num}`) };
print(2);//printing 2
print()// error
print(null)//printing null
非常整洁,不是吗?
格式化 json 代码
你可能对 JSON.stringify 非常熟悉。但是你是否知道可以用 stringify 进行格式化输出?实际上这很简单。
stringify 方法需要三个输入。 value,replacer 和 space。后两个是可选参数。这就是为什么我们以前没有注意过它们。要对 json 进行缩进,必须使用 space 参数。
console.log(JSON.stringify({name:"John",Age:23},null,'\t'));
>>>
{
"name": "John",
"Age": 23
}
从数组中获取唯一值
要从数组中获取唯一值,我们需要使用 filter 方法来过滤出重复值。但是有了新的 Set 对象,事情就变得非常顺利和容易了。
let uniqueArray = [...new Set([1, 2, 3, 3, 3, "school", "school", 'ball', false, false, true, true])];
>>> [1, 2, 3, "school", "ball", false, true]
从数组中删除虚值(Falsy Value)
在某些情况下,你可能想从数组中删除虚值。虚值是 JavaScript 的 Boolean 上下文中被认定为为 false 的值。 JavaScript 中只有六个虚值,它们是:
undefined
null
NaN
0
"" (空字符串)
false
滤除这些虚值的最简单方法是使用以下函数。
myArray.filter(Boolean);
如果要对数组进行一些修改,然后过滤新数组,可以尝试这样的操作。请记住,原始的 myArray 会保持不变。
myArray
.map(item => {
// Do your changes and return the new item
})
.filter(Boolean);
合并多个对象
假设我有几个需要合并的对象,那么这是我的首选方法。
const user = {
name: 'John Ludwig',
gender: 'Male'
};
const college = {
primary: 'Mani Primary School',
secondary: 'Lass Secondary School'
};
const skills = {
programming: 'Extreme',
swimming: 'Average',
sleeping: 'Pro'
};
const summary = {...user, ...college, ...skills};
这三个点在 JavaScript 中也称为展开运算符。你可以在这里学习更多用法。
对数字数组进行排序
JavaScript 数组有内置的 sort 方法。默认情况下 sort 方法把数组元素转换为字符串,并对其进行字典排序。在对数字数组进行排序时,这有可能会导致一些问题。所以下面是解决这类问题的简单解决方案。
[0,10,4,9,123,54,1].sort((a,b) => a-b);
>>> [0, 1, 4, 9, 10, 54, 123]
这里提供了一个将数字数组中的两个元素与 sort 方法进行比较的函数。这个函数可帮助我们接收正确的输出。
Disable Right Click
禁用右键
你可能想要阻止用户在你的网页上单击鼠标右键。
<body oncontextmenu="return false">
<div></div>
</body>
这段简单的代码将为你的用户禁用右键单击。
使用别名进行解构
解构赋值语法是一种 JavaScript 表达式,可以将数组中的值或对象的值或属性分配给变量。解构赋值能让我们用更简短的语法进行多个变量的赋值。
const object = { number: 10 };
// Grabbing number
const { number } = object;
// Grabbing number and renaming it as otherNumber
const { number: otherNumber } = object;
console.log(otherNumber); //10
获取数组中的最后一项
可以通过对 splice 方法的参数传入负整数,来数获取组末尾的元素。
let array = [0, 1, 2, 3, 4, 5, 6, 7]
console.log(array.slice(-1));
>>>[7]
console.log(array.slice(-2));
>>>[6, 7]
console.log(array.slice(-3));
>>>[5, 6, 7]
等待 Promise 完成
在某些情况下,你可能会需要等待多个 promise 结束。可以用 Promise.all 来并行运行我们的 promise。
const PromiseArray = [
Promise.resolve(100),
Promise.reject(null),
Promise.resolve("Data release"),
Promise.reject(new Error('Something went wrong'))];
Promise.all(PromiseArray)
.then(data => console.log('all resolved! here are the resolve values:', data))
.catch(err => console.log('got rejected! reason:', err))
关于 Promise.all 的主要注意事项是,当一个 Promise 拒绝时,该方法将引发错误。这意味着你的代码不会等到你所有的 promise 都完成。
如果你想等到所有 promise 都完成后,无论它们被拒绝还是被解决,都可以使用 Promise.allSettled。此方法在 ES2020 的最终版本得到支持。
const PromiseArray = [
Promise.resolve(100),
Promise.reject(null),
Promise.resolve("Data release"),
Promise.reject(new Error('Something went wrong'))];
Promise.allSettled(PromiseArray).then(res =>{
console.log(res);
}).catch(err => console.log(err));
//[
//{status: "fulfilled", value: 100},
//{status: "rejected", reason: null},
//{status: "fulfilled", value: "Data release"},
//{status: "rejected", reason: Error: Something went wrong ...}
//]
即使某些 promise 被拒绝,Promise.allSettled 也会从你所有的 promise 中返回结果。
XML(Extensible Markup Language 可扩展标记语言),XML是一个以文本来描述数据的文档。
<?xml version="1.0" encoding="UTF-8"?> <people> <person personid="E01"> <name>Tony</name> <address>10 Downing Street, London, UK</address> <tel>(061) 98765</tel> <fax>(061) 98765</fax> <email>tony@everywhere.com</email> </person> <person personid="E02"> <name>Bill</name> <address>White House, USA</address> <tel>(001) 6400 98765</tel> <fax>(001) 6400 98765</fax> <email>bill@everywhere.com</email> </person> </people>
(1)充当显示数据(以XML充当显示层)
(2)存储数据(存储层)的功能
(3)以XML描述数据,并在联系服务器与系统的其余部分之间传递。(传输数据的一样格式)
从某种角度讲,XML是数据封装和消息传递技术。
3.解析XML:// 创建SAX解析器工厂对象 SAXParserFactory spf = SAXParserFactory.newInstance(); // 使用解析器工厂创建解析器实例 SAXParser saxParser = spf.newSAXParser(); // 创建SAX解析器要使用的事件侦听器对象 PersonHandler handler = new PersonHandler(); // 开始解析文件 saxParser.parse( new File(fileName), handler);
3.2. DOM解析XML:
DOM:Document Object Model(文档对象模型)
DOM的特性:
定义一组 Java 接口,基于对象,与语言和平台无关将 XML 文档表示为树,在内存中解析和存储 XML 文档,允许随机访问文档的不同部分。
DOM解析XML
DOM的优点,由于树在内存中是持久的,因此可以修改后更新。它还可以在任何时候在树中上下导航,API使用起来也较简单。
DocumentBuilderFactory builder = DocumentBuilderFactory.newInstance(); DocumentBuilder db = builder.newDocumentBuilder(); db.parse("person.xml"); NodeList node_person = doc.getElementsByTagName("person");
3.3. JDOM解析XML:
JDOM是两位著名的 Java 开发人员兼作者,Brett Mclaughlin 和 Jason Hunter 的创作成果, 2000 年初在类似于Apache协议的许可下,JDOM作为一个开放源代码项目正式开始研发了。
JDOM 简化了与 XML 的交互并且比使用 DOM 实现更快,JDOM 与 DOM 主要有两方面不同。首先,JDOM 仅使用具体类而不使用接口。这在某些方面简化了 API,但是也限制了灵活性。第二,API 大量使用了 Collections 类,简化了那些已经熟悉这些类的 Java 开发者的使用。
解析步骤: (1)SAXBuilder sax = new SAXBuilder(); (2)Document doc = sax.build(….); (3)Element el = doc.getRootElement();(4)List list = el.getChildren(); (5)遍历内容
解析步骤: (1)SAXReader sax = new SAXReader(); (2)Document doc = sax.read(Thread.currentThread().getContextClassLoader() .getResourceAsStream("person.xml")); (3)Element root = doc.getRootElement(); (4)Iterator iterator = root.elementIterator(); (5)遍历迭代器
public class Person { private String personid; private String name; private String address; private String tel; private String fax; private String email; @Override public String toString() { return "Person{" + "personid='" + personid + '\'' + ", name='" + name + '\'' + ", address='" + address + '\'' + ", tel='" + tel + '\'' + ", fax='" + fax + '\'' + ", email='" + email + '\'' + '}'; } public String getPersonid() { return personid; } public void setPersonid(String personid) { this.personid = personid; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } public String getTel() { return tel; } public void setTel(String tel) { this.tel = tel; } public String getFax() { return fax; } public void setFax(String fax) { this.fax = fax; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } }
<?xml version="1.0" encoding="UTF-8"?> <people> <person personid="E01"> <name>Tony Blair</name> <address>10 Downing Street, London, UK</address> <tel>(061) 98765</tel> <fax>(061) 98765</fax> <email>blair@everywhere.com</email> </person> <person personid="E02"> <name>Bill Clinton</name> <address>White House, USA</address> <tel>(001) 6400 98765</tel> <fax>(001) 6400 98765</fax> <email>bill@everywhere.com</email> </person> </people>
import org.xml.sax.Attributes; import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; import java.util.ArrayList; import java.util.List; /** * Created by Hu Guanzhong * SAX解析的特点: * 1、基于事件驱动 * 2、顺序读取,速度快 * 3、不能任意读取节点(灵活性差) * 4、解析时占用的内存小 * 5、SAX更适用于在性能要求更高的设备上使用(Android开发中) * */ public class PersonHandler extends DefaultHandler{ private List<Person> persons = null; private Person p;//当前正在解析的person private String tag;//用于记录当前正在解析的标签名 public List<Person> getPersons() { return persons; } //开始解析文档时调用 @Override public void startDocument() throws SAXException { super.startDocument(); persons = new ArrayList<>(); System.out.println("开始解析文档..."); } //在XML文档解析结束时调用 @Override public void endDocument() throws SAXException { super.endDocument(); System.out.println("解析文档结束."); } /** * 解析开始元素时调用 * @param uri 命名空间 * @param localName 不带前缀的标签名 * @param qName 带前缀的标签名 * @param attributes 当前标签的属性集合 * @throws SAXException */ @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { super.startElement(uri, localName, qName, attributes); if ("person".equals(qName)){ p = new Person(); String personid = attributes.getValue("personid"); p.setPersonid(personid); } tag = qName; System.out.println("startElement--"+qName); } //解析结束元素时调用 @Override public void endElement(String uri, String localName, String qName) throws SAXException { super.endElement(uri, localName, qName); if ("person".equals(qName)) { persons.add(p); } tag = null; System.out.println("endElement--"+qName); } //解析文本内容时调用 @Override public void characters(char[] ch, int start, int length) throws SAXException { super.characters(ch, start, length); if (tag != null) { if ("name".equals(tag)) { p.setName(new String(ch,start,length)); }else if("address".equals(tag)){ p.setAddress(new String(ch,start,length)); }else if("tel".equals(tag)){ p.setTel(new String(ch,start,length)); }else if("fax".equals(tag)){ p.setFax(new String(ch,start,length)); }else if("email".equals(tag)){ p.setEmail(new String(ch,start,length)); } System.out.println(ch); } } }
public class XMLDemo { /** * 使用第三方xstream组件实现XML的解析与生成 */ @Test public void xStream(){ Person p = new Person(); p.setPersonid("1212"); p.setAddress("北京"); p.setEmail("vince@163.com"); p.setFax("6768789798"); p.setTel("13838389438"); p.setName("38"); XStream xStream = new XStream(new Xpp3Driver()); xStream.alias("person",Person.class); xStream.useAttributeFor(Person.class,"personid"); String xml = xStream.toXML(p); System.out.println(xml); //解析XML Person person = (Person)xStream.fromXML(xml); System.out.println(person); } /** * 从XML文件中读取对象 */ @Test public void xmlDecoder() throws FileNotFoundException { BufferedInputStream in = new BufferedInputStream(new FileInputStream("test.xml")); XMLDecoder decoder = new XMLDecoder(in); Person p = (Person)decoder.readObject(); System.out.println(p); } /** * 把对象转成XML文件写入 */ @Test public void xmlEncoder() throws FileNotFoundException { BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("test.xml")); XMLEncoder xmlEncoder = new XMLEncoder(bos); Person p = new Person(); p.setPersonid("1212"); p.setAddress("北京"); p.setEmail("vince@163.com"); p.setFax("6768789798"); p.setTel("13838389438"); p.setName("38"); xmlEncoder.writeObject(p); xmlEncoder.close(); } /** * DOM4J解析XML * 基于树型结构,第三方组件 * 解析速度快,效率更高,使用的JAVA中的迭代器实现数据读取,在WEB框架中使用较多(Hibernate) * */ @Test public void dom4jParseXML() throws DocumentException { //1 创建DOM4J的解析器对象 SAXReader reader = new SAXReader(); InputStream is = Thread.currentThread().getContextClassLoader() .getResourceAsStream("com/vince/xml/person.xml"); org.dom4j.Document doc = reader.read(is); org.dom4j.Element rootElement = doc.getRootElement(); Iterator<org.dom4j.Element> iterator = rootElement.elementIterator(); ArrayList<Person> persons = new ArrayList<>(); Person p = null; while(iterator.hasNext()){ p = new Person(); org.dom4j.Element e = iterator.next(); p.setPersonid(e.attributeValue("personid")); Iterator<org.dom4j.Element> iterator1 = e.elementIterator(); while(iterator1.hasNext()){ org.dom4j.Element next = iterator1.next(); String tag = next.getName(); if("name".equals(tag)){ p.setName(next.getText()); }else if("address".equals(tag)){ p.setAddress(next.getText()); }else if("tel".equals(tag)){ p.setTel(next.getText()); }else if("fax".equals(tag)){ p.setFax(next.getText()); }else if("email".equals(tag)){ p.setEmail(next.getText()); } } persons.add(p); } System.out.println("结果:"); System.out.println(Arrays.toString(persons.toArray())); } /** * JDOM解析 XML * 1、与DOM类似基于树型结构, * 2、与DOM的区别: * (1)第三方开源的组件 * (2)实现使用JAVA的Collection接口 * (3)效率比DOM更快 */ @Test public void jdomParseXML() throws JDOMException, IOException { //创建JDOM解析器 SAXBuilder builder = new SAXBuilder(); InputStream is = Thread.currentThread().getContextClassLoader() .getResourceAsStream("com/vince/xml/person.xml"); org.jdom2.Document build = builder.build(is); Element rootElement = build.getRootElement(); List<Person> list = new ArrayList<>(); Person person = null; List<Element> children = rootElement.getChildren(); for(Element element: children){ person = new Person(); String personid = element.getAttributeValue("personid"); person.setPersonid(personid); List<Element> children1 = element.getChildren(); for (Element e: children1){ String tag = e.getName(); if("name".equals(tag)){ person.setName(e.getText()); }else if("address".equals(tag)){ person.setAddress(e.getText()); }else if("tel".equals(tag)){ person.setTel(e.getText()); }else if("fax".equals(tag)){ person.setFax(e.getText()); }else if("email".equals(tag)){ person.setEmail(e.getText()); } } list.add(person); } System.out.println("结果:"); System.out.println(Arrays.toString(list.toArray())); } /** * DOM解析XML * 1、基于树型结构,通过解析器一次性把文档加载到内存中,所以会比较占用内存,可以随机访问 * 更加灵活,更适合在WEB开发中使用 */ @Test public void domParseXML() throws ParserConfigurationException, IOException, SAXException { //1、创建一个DOM解析器工厂对象 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); //2、通过工厂对象创建解析器对象 DocumentBuilder documentBuilder = factory.newDocumentBuilder(); //3、解析文档 InputStream is = Thread.currentThread().getContextClassLoader() .getResourceAsStream("com/vince/xml/person.xml"); //此代码完成后,整个XML文档已经被加载到内存中,以树状形式存储 Document doc = documentBuilder.parse(is); //4、从内存中读取数据 //获取节点名称为person的所有节点,返回节点集合 NodeList personNodeList = doc.getElementsByTagName("person"); ArrayList<Person> persons = new ArrayList<>(); Person p = null; //此循环会迭代两次 for (int i=0;i<personNodeList.getLength();i++){ Node personNode = personNodeList.item(i); p = new Person(); //获取节点的属性值 String personid = personNode.getAttributes().getNamedItem("personid").getNodeValue(); p.setPersonid(personid); //获取当前节点的所有子节点 NodeList childNodes = personNode.getChildNodes(); for (int j = 0;j<childNodes.getLength();j++){ Node item = childNodes.item(j); String nodeName = item.getNodeName(); if ("name".equals(nodeName)) { p.setName(item.getFirstChild().getNodeValue()); }else if("address".equals(nodeName)){ p.setAddress(item.getFirstChild().getNodeValue()); }else if("tel".equals(nodeName)){ p.setTel(item.getFirstChild().getNodeValue()); }else if("fax".equals(nodeName)){ p.setFax(item.getFirstChild().getNodeValue()); }else if("email".equals(nodeName)){ p.setEmail(item.getFirstChild().getNodeValue()); } } persons.add(p); } System.out.println("结果:"); System.out.println(Arrays.toString(persons.toArray())); } /** * SAX解析的特点: * 1、基于事件驱动 * 2、顺序读取,速度快 * 3、不能任意读取节点(灵活性差) * 4、解析时占用的内存小 * 5、SAX更适用于在性能要求更高的设备上使用(Android开发中) * @throws ParserConfigurationException * @throws SAXException * @throws IOException */ @Test public void saxParseXML() throws ParserConfigurationException, SAXException, IOException { //1、创建一个SAX解析器工厂对象 SAXParserFactory saxParserFactory = SAXParserFactory.newInstance(); //2、通过工厂对象创建SAX解析器 SAXParser saxParser = saxParserFactory.newSAXParser(); //3、创建一个数据处理器(需要我们自己来编写) PersonHandler personHandler = new PersonHandler(); //4、开始解析 InputStream is = Thread.currentThread().getContextClassLoader() .getResourceAsStream("com/vince/xml/person.xml"); saxParser.parse(is,personHandler); List<Person> persons = personHandler.getPersons(); for (Person p:persons){ System.out.println(p); } } }
背景
这一个因为滚动条占据空间引起的bug, 查了一下资料, 最后也解决了,顺便研究一下这个属性, 做一下总结,分享给大家看看。
正文
昨天, 测试提了个问题, 现象是一个输入框的聚焦提示偏了, 让我修一下, 如下图:
image.png
起初认为是红框提示位置不对, 就去找代码看:
<Input
// ...
onFocus={() => setFocusedInputName('guidePrice')}
onBlur={() => setFocusedInputName('')}
/>
<Table
data-focused-column={focusedInputName}
// ...
/>
代码上没有什么问题, 不是手动设置的,而且, 在我和另一个同事, 还有PM的PC上都是OK的:
image.png
初步判断是,红框位置结算有差异, 差异大小大概是17px, 但是这个差异是怎么产生的呢?
就去测试小哥的PC上看, 注意到一个细节, 在我PC上, 滚动条是悬浮的:
image.png
在他PC上, 滚动条是占空间的:
image.png
在他电脑上, 手动把原本的 overscroll-y: scroll 改成 overscroll-y: overlay 问题就结局了。
由此判定是: 滚动条占据空间 引起的bug。
overscroll-y: overlay
CSS属性 overflow, 定义当一个元素的内容太大而无法适应块级格式化上下文的时候该做什么。它是 overflow-x 和overflow-y的 简写属性 。
/* 默认值。内容不会被修剪,会呈现在元素框之外 */
overflow: visible;
/* 内容会被修剪,并且其余内容不可见 */
overflow: hidden;
/* 内容会被修剪,浏览器会显示滚动条以便查看其余内容 */
overflow: scroll;
/* 由浏览器定夺,如果内容被修剪,就会显示滚动条 */
overflow: auto;
/* 规定从父元素继承overflow属性的值 */
overflow: inherit;
官方描述:
overlay 行为与 auto 相同,但滚动条绘制在内容之上而不是占用空间。 仅在基于 WebKit(例如,Safari)和基于Blink的(例如,Chrome或Opera)浏览器中受支持。
表现:
html {
overflow-y: overlay;
}
兼容性
没有在caniuse上找到这个属性的兼容性, 也有人提这个问题:
image.png
问题场景以及解决办法
1. 外部容器的滚动条
这里的外部容器指的是html, 直接加在最外层:
html {
overflow-y: scroll;
}
手动加上这个特性, 不论什么时候都有滚动宽度占据空间。
缺点: 没有滚动的时候也会有个滚动条, 不太美观。
优点: 方便, 没有兼容性的问题。
2. 外部容器绝对定位法
用绝对定位,保证了body的宽度一直保持完整空间:
html {
overflow-y: scroll; // 兼容ie8,不支持:root, vw
}
:root {
overflow-y: auto;
overflow-x: hidden;
}
:root body {
position: absolute;
}
body {
width: 100vw;
overflow: hidden;
}
3. 内部容器做兼容
.wrapper {
overflow-y: scroll; // fallback
overflow-y: overlay;
}
总结
个人推荐还是用 overlay, 然后使用scroll 做为兜底。
内容就这么多, 希望对大家有所启发。
文章如有错误, 请在留言区指正, 谢谢。
之前花了些时间将gatsby-theme-gitbook迁移到 Typescript,以获得在 VSCode 中更好的编程体验.
整体差不多已经完成迁移,剩下将 Gatsby 的 API 文件也迁移到 TS,这里可以看到 gatsby#21995 官方也在将核心代码库迁移到 Typescript,准备等待官方将核心代码库迁移完成,在迁移 API 文件.
这篇文章用XYShaoKang/gatsby-project-config,演示如何将 gatsby 迁移到 TypeScript,希望能帮到同样想要在 Gatsby 中使用 TS 的同学.
迁移步骤:
TS 配置
配置 ESLint 支持 TS
完善 GraphQL 类型提示
初始化项目
gatsby new gatsby-migrate-to-typescript XYShaoKang/gatsby-project-config
cd gatsby-migrate-to-typescript
yarn develop
TS 配置
安装typescript
添加typescript.json配置文件
修改 js 文件为 tsx
补全 TS 声明定义
安装typescript
yarn add -D typescript
添加配置文件tsconfig.json
// https://www.typescriptlang.org/v2/docs/handbook/tsconfig-json.html
{
"compilerOptions": {
"target": "esnext", // 编译生成的目标 es 版本,可以根据需要设置
"module": "esnext", // 编译生成的目标模块系统
"lib": ["dom", "es2015", "es2017"], // 配置需要包含的运行环境的类型定义
"jsx": "react", // 配置 .tsx 文件的输出模式
"strict": true, // 开启严格模式
"esModuleInterop": true, // 兼容 CommonJS 和 ES Module
"moduleResolution": "node", // 配置模块的解析规则,支持 node 模块解析规则
"noUnusedLocals": true, // 报告未使用的局部变量的错误
"noUnusedParameters": true, // 报告有关函数中未使用参数的错误
"experimentalDecorators": true, // 启用装饰器
"emitDecoratorMetadata": true, // 支持装饰器上生成元数据,用来进行反射之类的操作
"noEmit": true, // 不输出 js,源映射或声明之类的文件,单纯用来检查错误
"skipLibCheck": true // 跳过声明文件的类型检查,只会检查已引用的部分
},
"exclude": ["./node_modules", "./public", "./.cache"], // 解析时,应该跳过的路晋
"include": ["src"] // 定义包含的路径,定义在其中的声明文件都会被解析进 vscode 的智能提示
}
将index.js改成index.tsx,重新启动服务,查看效果.
其实 Gatsby 内置了支持 TS,不用其他配置,只要把index.js改成index.tsx就可以直接运行.添加 TS 依赖是为了显示管理 TS,而tsconfig.json也是这个目的,当我们有需要新的特性以及自定义配置时,可以手动添加.
补全 TS 声明定义
打开index.tsx,VSCode 会报两个错误,一个是找不到styled-components的声明文件,这个可以通过安装@types/styled-components来解决.
另外一个错误绑定元素“data”隐式具有“any”类型。,这个错误是因为我们在tsconfig.json中指定了"strict": true,这会开启严格的类型检查,可以通过关闭这个选项来解决,只是我们用 TS 就是要用它的类型检查的,所以正确的做法是给data定义类型.
下面来一一修复错误.
安装styled-components的声明文件
yarn add -D @types/styled-components
修改index.tsx
import React, { FC } from 'react'
import styled from 'styled-components'
import { graphql } from 'gatsby'
import { HomeQuery } from './__generated__/HomeQuery'
const Title = styled.h1`
font-size: 1.5em;
margin: 0;
padding: 0.5em 0;
color: palevioletred;
background: papayawhip;
`
const Content = styled.div`
margin-top: 0.5em;
`
interface PageQuery {
data: {
allMarkdownRemark: {
edges: Array<{
node: {
frontmatter: {
title: string
}
excerpt: string
}
}>
}
}
}
const Home: FC<PageQuery> = ({ data }) => {
const node = data.allMarkdownRemark.edges[0].node
const title = node.frontmatter?.title
const excerpt = node.excerpt
return (
<>
<Title>{title}</Title>
<Content>{excerpt}</Content>
</>
)
}
export default Home
export const query = graphql`
query HomeQuery {
allMarkdownRemark {
edges {
node {
frontmatter {
title
}
excerpt
}
}
}
}
`
这时候会出现一个新的错误,在excerpt: string处提示Parsing error: Unexpected token,这是因为 ESLint 还无法识别 TS 的语法,下面来配置 ESLint 支持 TS.
配置 ESLint 支持 TypeScript
安装依赖
yarn add -D @typescript-eslint/parser @typescript-eslint/eslint-plugin
配置.eslintrc.js
module.exports = {
parser: `@typescript-eslint/parser`, // 将解析器从`babel-eslint`替换成`@typescript-eslint/parser`,用以解析 TS 代码
extends: [
`google`,
`eslint:recommended`,
`plugin:@typescript-eslint/recommended`, // 使用 @typescript-eslint/eslint-plugin 推荐配置
`plugin:react/recommended`,
`prettier/@typescript-eslint`, // 禁用 @typescript-eslint/eslint-plugin 中与 prettier 冲突的规则
`plugin:prettier/recommended`,
],
plugins: [
`@typescript-eslint`, // 处理 TS 语法规则
`react`,
`filenames`,
],
// ...
}
在.vscode/settings.json中添加配置,让VSCode使用ESLint扩展格式化ts和tsx文件
// .vscode/settings.json
{
"eslint.format.enable": true,
"[javascript]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
},
"[javascriptreact]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
},
"[typescript]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
},
"[typescriptreact]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
}
}
完善 GraphQL 类型提示
// index.tsx
import React, { FC } from 'react'
// ...
interface PageQuery {
data: {
allMarkdownRemark: {
edges: Array<{
node: {
frontmatter: {
title: string
}
excerpt: string
}
}>
}
}
}
const Home: FC<PageQuery> = ({ data }) => {
// ...
}
export default Home
export const query = graphql`
query HomeQuery {
allMarkdownRemark {
edges {
node {
frontmatter {
title
}
excerpt
}
}
}
}
`
我们看看index.tsx文件,会发现PropTypes和query结构非常类似,在Gatsby运行时,会把query查询的结果作为组件prop.data传入组件,而PropTypes是用来约束prop存在的.所以其实PropTypes就是根据query写出来的.
如果有依据query自动生成PropTypes的功能就太棒了.
另外一个问题是在query中编写GraphQL查询时,并没有类型约束,也没有智能提示.
总结以下需要完善的体验包括:
GraphQL 查询编写时的智能提示,以及错误检查
能够从 GraphQL 查询生成对应的 TypeScript 类型.这样能保证类型的唯一事实来源,并消除 TS 中冗余的类型声明.毕竟如果经常需要手动更新两处类型,会更容易出错,而且也并不能保证手动定义类型的正确性.
实现方式:
通过生成架构文件,配合Apollo GraphQL for VS Code插件,实现智能提示,以及错误检查
通过graphql-code-generator或者apollo生成 TS 类型定义文件
如果自己去配置的话,是挺耗费时间的,需要去了解graphql-code-generator的使用,以及Apollo的架构等知识.
不过好在社区中已经有对应的 Gatsby 插件集成了上述工具可以直接使用,能让我们不用去深究对应知识的情况下,达到优化 GraphQL 编程的体验.
尝试过以下两个插件能解决上述问题,可以任选其一使用
gatsby-plugin-codegen
gatsby-plugin-typegen
另外还有一款插件gatsby-plugin-graphql-codegen也可以生成 TS 类型,不过配置略麻烦,并且上述两个插件都可以满足我现在的需求,所以没有去尝试,感兴趣的可以尝试一下.
注意点:
Apollo不支持匿名查询,需要使用命名查询
第一次生成,需要运行Gatsby之后才能生成类型文件
整个项目内不能有相同命名的查询,不然会因为名字有冲突而生成失败
下面是具体操作
安装vscode-apollo扩展
在 VSCode 中按 Ctrl + P ( MAC 下: Cmd + P) 输入以下命令,按回车安装
ext install apollographql.vscode-apollo
方式一: 使用gatsby-plugin-codegen
gatsby-plugin-codegen默认会生成apollo.config.js和schema.json,配合vscode-apollo扩展,可以提供GraphQL的类型约束和智能提示.
另外会自动根据query中的GraphQL查询,生成 TS 类型,放在对应的tsx文件同级目录下的__generated__文件夹,使用时只需要引入即可.
如果需要在运行时自动生成 TS 类型,需要添加watch: true配置.
安装gatsby-plugin-codegen
yarn add gatsby-plugin-codegen
配置gatsby-config.js
// gatsby-config.js
module.exports = {
plugins: [
// ...
{
resolve: `gatsby-plugin-codegen`,
options: {
watch: true,
},
},
],
}
重新运行开发服务生成类型文件
yarn develop
如果出现以下错误,一般是因为没有为查询命名的缘故,给查询添加命名即可,另外配置正确的话,打开对应的文件,有匿名查询,编辑器会有错误提示.
fix-anonymous-operations.png
这个命名之后会作为生成的类型名.
修改index.tsx以使用生成的类型
gatsby-plugin-codegen插件会更具查询生成对应的查询名称的类型,保存在对应tsx文件同级的__generated__目录下.
import { HomeQuery } from './__generated__/HomeQuery' // 引入自动生成的类型
// ...
// interface PageQuery {
// data: {
// allMarkdownRemark: {
// edges: Array<{
// node: {
// frontmatter: {
// title: string
// }
// excerpt: string
// }
// }>
// }
// }
// }
interface PageQuery {
data: HomeQuery // 替换之前手写的类型
}
// ...
将自动生成的文件添加到.gitignore中
apollo.config.js,schema.json,__generated__能通过运行时生成,所以可以添加到.gitignore中,不用提交到 git 中.当然如果有需要也可以选择提交到 git 中.
# Generated types by gatsby-plugin-codegen
__generated__
apollo.config.js
schema.json
方式二: 使用gatsby-plugin-typegen
gatsby-plugin-typegen通过配置生成gatsby-schema.graphql和gatsby-plugin-documents.graphql配合手动创建的apollo.config.js提供GraphQL的类型约束和智能提示.
根据GraphQL查询生成gatsby-types.d.ts,生成的类型放在命名空间GatsbyTypes下,使用时通过GatsbyTypes.HomeQueryQuery来引入,HomeQueryQuery是由对应的命名查询生成
安装gatsby-plugin-typegen
yarn add gatsby-plugin-typegen
配置
// gatsby-config.js
module.exports = {
plugins: [
// ...
{
resolve: `gatsby-plugin-typegen`,
options: {
outputPath: `src/__generated__/gatsby-types.d.ts`,
emitSchema: {
'src/__generated__/gatsby-schema.graphql': true,
},
emitPluginDocuments: {
'src/__generated__/gatsby-plugin-documents.graphql': true,
},
},
},
],
}
//apollo.config.js
module.exports = {
client: {
tagName: `graphql`,
includes: [
`./src/**/*.{ts,tsx}`,
`./src/__generated__/gatsby-plugin-documents.graphql`,
],
service: {
name: `GatsbyJS`,
localSchemaFile: `./src/__generated__/gatsby-schema.graphql`,
},
},
}
重新运行开发服务生成类型文件
yarn develop
修改index.tsx以使用生成的类型
gatsby-plugin-codegen插件会更具查询生成对应的查询名称的类型,保存在对应tsx文件同级的__generated__目录下.
// ...
// interface PageQuery {
// data: {
// allMarkdownRemark: {
// edges: Array<{
// node: {
// frontmatter: {
// title: string
// }
// excerpt: string
// }
// }>
// }
// }
// }
interface PageQuery {
data: GatsbyTypes.HomeQueryQuery // 替换之前手写的类型
}
// ...
将自动生成的文件添加到.gitignore中
__generated__能通过运行时生成,所以可以添加到.gitignore中,不用提交到 git 中.当然如果有需要也可以选择提交到 git 中.
# Generated types by gatsby-plugin-codegen
__generated__
Canvas 是 HTML5 提供的一个用于展示绘图效果的标签. Canvas 原意为画布, 在 HTML 页面中用于展示绘图效果. 最早 Canvas 是苹果提出的一个方案, 今天已经在大多数浏览器中实现。
canvas 的使用领域
游戏
大数据可视化数据
banner 广告
多媒体
模拟仿真
远程操作
图形编辑
判断浏览器是否支持 canvas 标签
var canvas = document.getElementById('canvas')
if (canvas.getContext) {
console.log('你的浏览器支持Canvas!')
} else {
console.log('你的浏览器不支持Canvas!')
}
canvas 的基本用法
1、使用 canvas 标签, 即可在页面中开辟一格区域,可以设置其宽高,宽高为 300 和 150
<canvas></canvas>
2、获取 dom 元素 canvas
canvas 本身不能绘图. 是使用 JavaScript 来完成绘图. canvas 对象提供了各种绘图用的 api。
var cas = document.querySelector('canvas')
3、通过 cas 获取上下文对象(画布对象!)
var ctx = cas.getContext('2d')
4、通过 ctx 开始画画(设置起点 设置终点 连线-描边 )
ctx.moveTo(10, 10)
ctx.lineTo(100, 100)
ctx.stroke()
绘制线条
设置开始位置: context.moveTo( x, y )
设置终点位置: context.lineTo( x, y )
描边绘制: context.stroke()
填充绘制: context.fill()
闭合路径: context.closePath()
canvas 还可以设置线条的相关属性,如下:
CanvasRenderingContext2D.lineWidth 设置线宽.
CanvasRenderingContext2D.strokeStyle 设置线条颜色.
CanvasRenderingContext2D.lineCap 设置线末端类型,'butt'( 默认 ), 'round', 'square'.
CanvasRenderingContext2D.lineJoin 设置相交线的拐点, 'miter'(默认),'round', 'bevel',
CanvasRenderingContext2D.getLineDash() 获得线段样式数组.
CanvasRenderingContext2D.setLineDash() 设置线段样式.
CanvasRenderingContext2D.lineDashOffset 绘制线段偏移量.
封装一个画矩形的方法
function myRect(ctxTmp, x, y, w, h) {
ctxTmp.moveTo(x, y)
ctxTmp.lineTo(x + w, y)
ctxTmp.lineTo(x + w, y + h)
ctxTmp.lineTo(x, y + h)
ctxTmp.lineTo(x, y)
ctxTmp.stroke()
}
var cas = document.querySelector('canvas')
var ctx = cas.getContext('2d')
myRect(ctx, 50, 50, 200, 200)
绘制矩形
fillRect( x , y , width , height) 填充以(x,y)为起点宽高分别为 width、height 的矩形 默认为黑色
stokeRect( x , y , width , height) 绘制一个空心以(x,y)为起点宽高分别为 width、height 的矩形
clearRect( x, y , width , height ) 清除以(x,y)为起点宽高分别为 width、height 的矩形 为透明
绘制圆弧
绘制圆弧的方法有
CanvasRenderingContext2D.arc()
CanvasRenderingContext2D.arcTo()
6 个参数: x,y(圆心的坐标),半径,起始的弧度(不是角度 deg),结束的弧度,(bool 设置方向 ! )
var cas = document.querySelector('canvas')
var ctx = cas.getContext('2d')
ctx.arc(100, 100, 100, 0, degToArc(360))
ctx.stroke()
// 角度转弧度
function degToArc(num) {
return (Math.PI / 180) * num
}
绘制扇形
var cas = document.querySelector('canvas')
var ctx = cas.getContext('2d')
ctx.arc(300, 300, 200, degToArc(125), degToArc(300))
// 自动连回原点
ctx.closePath()
ctx.stroke()
function degToArc(num) {
return (Math.PI / 180) * num
}
制作画笔
声明一个变量作为标识
鼠标按下的时候,记录起点位置
鼠标移动的时候,开始描绘并连线
鼠标抬起的时候,关闭开关
点击查看效果图
var cas = document.querySelector('canvas')
var ctx = cas.getContext('2d')
var isDraw = false
// 鼠标按下事件
cas.addEventListener('mousedown', function () {
isDraw = true
ctx.beginPath()
})
// 鼠标移动事件
cas.addEventListener('mousemove', function (e) {
if (!isDraw) {
// 没有按下
return
}
// 获取相对于容器内的坐标
var x = e.offsetX
var y = e.offsetY
ctx.lineTo(x, y)
ctx.stroke()
})
cas.addEventListener('mouseup', function () {
// 关闭开关了!
isDraw = false
})
手动涂擦
原理和画布相似,只不过用的是clearRect()方法。
点击查看效果图
var cas = document.querySelector('canvas')
var ctx = cas.getContext('2d')
ctx.fillRect(0, 0, 600, 600)
// 开关
var isClear = false
cas.addEventListener('mousedown', function () {
isClear = true
})
cas.addEventListener('mousemove', function (e) {
if (!isClear) {
return
}
var x = e.offsetX
var y = e.offsetY
var w = 20
var h = 20
ctx.clearRect(x, y, w, h)
})
cas.addEventListener('mouseup', function () {
isClear = false
})
刮刮乐
首先需要设置奖品和画布,将画布置于图片上方盖住,
随机设置生成奖品。
当手触摸移动的时候,可以擦除部分画布,露出奖品区。
点击查看效果图
<div>
<img src="./images/2.jpg" alt="" />
<canvas width="600" height="600"></canvas>
</div>
css
img {
width: 600px;
height: 600px;
position: absolute;
top: 10%;
left: 30%;
}
canvas {
width: 600px;
height: 600px;
position: absolute;
top: 10%;
left: 30%;
border: 1px solid #000;
}
js
var cas = document.querySelector('canvas')
var ctx = cas.getContext('2d')
var img = document.querySelector('img')
// 加一个遮罩层
ctx.fillStyle = '#ccc'
ctx.fillRect(0, 0, cas.width, cas.height)
setImgUrl()
// 开关
var isClear = false
cas.addEventListener('mousedown', function () {
isClear = true
})
cas.addEventListener('mousemove', function (e) {
if (!isClear) {
return
}
var x = e.offsetX
var y = e.offsetY
ctx.clearRect(x, y, 30, 30)
})
cas.addEventListener('mouseup', function () {
isClear = false
})
function setImgUrl() {
var arr = ['./images/1.jpg', './images/2.jpg', './images/3.jpg', './images/4.jpg']
// 0-3
var random = Math.round(Math.random() * 3)
img.src = arr[random]
}
更多demo,请查看 github.com/Michael-lzg…
简单来说,v-if 的初始化较快,但切换代价高;v-show 初始化慢,但切换成本低
都是动态显示DOM元素
(1)手段:
v-if是动态的向DOM树内添加或者删除DOM元素;
v-show是通过设置DOM元素的display样式属性控制显隐;
(2)编译过程:
v-if切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;
v-show只是简单的基于css切换;
(3)编译条件:
v-if是惰性的,如果初始条件为假,则什么也不做;只有在条件第一次变为真时才开始局部编译(编译被缓存?编译被缓存后,然后再切换的时候进行局部卸载);
v-show是在任何条件下(首次条件是否为真)都被编译,然后被缓存,而且DOM元素保留;
(4)性能消耗:
v-if有更高的切换消耗;
v-show有更高的初始渲染消耗;
(5)使用场景:
v-if适合运营条件不大可能改变;
v-show适合频繁切换。
虽然Vue 3还没有正式发布,但是热爱新技术的我早已按捺不住自己的内心,开始尝试在小项目中使用它了。
根据这篇《今日凌晨Vue3 beta版震撼发布,竟然公开支持脚手架项目!》我搭建了一个Vue 3的脚手架项目,用这种方式搭建的脚手架项目不仅仅只有vue是新版的,就连vue-router、vuex都是的。
给大家截一下package.json的图:
可以看到vue-router和vuex都已经开启4.0时代啦!
不过其实我并没有去了解过vue-router 4.0的新用法什么的,因为我觉得它不像vue 3.0都已经进行到beta的版本不会有特别大的变动。
而vue-router 4.0还是alpha的阶段,所以我认为现在去学习它有些为时尚早。但却就是它!差点酿成了一场惨剧。
旧版vue + vue-router的使用方式
假如你在路由里面定义了一个动态参数通常都会这么写:
{
path: '/:id'
}
然后用编程式导航的时候通常会这样去写:
this.$router.push('/123')
在组件中是这样获取这个参数的:
this.$route.params.id
我以为的新版vue + vue-router的使用方式
由于vue 3.0的Composition API中没有this了,所以我想到了通过获取组件实例的方式来获取$route:
import { defineComponent, getCurrentInstance } from 'vue'
export default defineComponent((props, context) => {
const { ctx } = getCurrentInstance()
console.log(ctx.$route)
})
没想到打印出来的居然是undefined!
这是咋回事呢?
于是我又打印了一遍ctx(ctx是当前组件上下文):
没有$的那些字段是我在组件中自己定义的变量,带$的这些就是vue内置的了,找了半天发现没有$route了,只剩下了一个$router,估计vue-router 4.0把当前路由信息都转移到$router里面去了。
带着猜想,我点开了$router:
currentRoute! 看名字的话感觉应该就是它了!于是乎我:
import { defineComponent, getCurrentInstance } from 'vue'
export default defineComponent((props, context) => {
const { ctx } = getCurrentInstance()
console.log(ctx.$router.currentRoute.value.params.id)
})
果然获取到了!好开心!
实际的新版vue + vue-router用法
在接下来的过程中我用ctx.$router代替了原来的this.$router、用ctx.$router.currentRoute.value代替了原先的this.$route。
尽管在接下来的进度中并没有出现任何的bug,程序一直都是按照我所设想的那样去运行的。
但在项目打包后却出现了意想不到的bug:在跳转路由的时候报了一个在undefined上面没有push的错误。
奇了怪了,在开发阶段程序都没有任何的报错怎么一打包就不行了呢?根据我多年的开发经验,我很快就定位到了是vue-router的错误。
难道这样写是错的吗?可是我打印了ctx,它里面明明有一个$router、$router里面明明就有currentRoute、currentRoute里面明明就有一个value、value里面明明就有params、params里面我一点开明明就看到了传过来的参数啊:
估计可能是vue-router的bug,果然alpha阶段的产物不靠谱,我开始后悔使用新版的vue脚手架项目了。
vue-router里的hooks
不过这时我突然灵光一现,vue 3不是受到了react hooks的启发才产生了Composition API的吗?
那么估计vue-router肯定也会受到react-router的启发了!
还好我学过react,果然技多不压身啊!估计里面肯定是有一个useXxx,就像这样:
import { useXxx } from 'vue-router'
那么应该是use什么呢?按理来说应该会尽量的和以前的API保持一定的联系,我猜应该是useRoute和useRouter吧!
为了验证我的想法,我打开了node_modules找到了vue-router的源码:
果不其然,在第2454和第2455行我发现它导出了useRoute和useRouter,那么就是它了:
import { defineComponent } from 'vue'
import { useRoute, useRouter } from 'vue-router'
export default defineComponent(_ => {
const route = useRoute()
const router = useRouter()
console.log(route.params.id)
router.push('/xxx/xxx')
})
使用这种方式不但可以成功跳转路由,也同样可以获取到路由传过来的参数,这次再打包试了一下,果然就没有之前的那个报错了。
结语
估计以后的vue全家桶要开启全民hooks的时代了,在翻看源码的同时我发现他们把一些示例都写在了vue-router/playground文件夹下了,在里面我发现了一些有趣的用法。
如果有时间的话我会仔细研究一下然后出一篇更加深入的文章给大家,当然如果已经有小伙伴等不及我出新文章的话可以直接进入vue-router-next的github地址:
https://github.com/vuejs/vue-router-next
它的示例都放在了playground这个文件夹下,期待你们研究明白后出一篇更加深入的文章!
蓝蓝设计的小编 http://www.lanlanwork.com