上一篇博客讲到了数组的方法,当然里边比较复杂的就是数组的迭代方法,因为涉及到了回调函数,所以这篇博客我们来详细讲解一下js数组迭代方法的使用。
var arr=[1,2,3,4,5,6];
arr.forEach(function(val,index,arr){
console.log(val,index,arr);
})
// 其中:
// value:每一个数组项的值 必填项
// index:每一个数组项对应的索引
// arr:当前的数组
注意:forEach()方法不返回值,所以回调函数中使用return会打印出来undefined。
var aNum2 = [1.2, 1.8, 2.0, 4.3];
console.log(aNum2.map(Math.floor()));// [1,1,2,4]
var arr=[1,2,3];
console.log(arr.map(function(val,index){
return val*3
}));// 3 6 9
// 其中:
// value:每一个数组项的值 必填项
// index:每一个数组项对应的索引
// arr:当前的数组
注意:map()方法有返回值,返回值为新的数组,所以可以直接再回调函数中return。
var arr=[10,20,30];
console.log(arr.every(function(val){
return val>20;
}));// false
console.log(arr.every(function(val){
return val>0;
}));// true
// 其中:
// value:每一个数组项的值 必填项
// index:每一个数组项对应的索引
// arr:当前的数组
注意:every()方法所有的数组项都符合判断时返回true,否则返回false。
var arr=[10,20,30];
console.log(arr.some(function(val){
return val>20;
}));// true
console.log(arr.some(function(val){
return val>0;
}));// true
console.log(arr.some(function(val){
return val<0;
}));// false
arr.some(function(val){
console.log(val<0);
});//fasle false false
// 其中:
// value:每一个数组项的值 必填项
// index:每一个数组项对应的索引
// arr:当前的数组
注意:some()方法如果回调函数执行完会根据结果返回true或false,但是回调函数中打印判断是,只会作为判断条件的返回值,则会打印多遍。
5.fliter(funcrion(value,index,arr){}):对数组的每一项都运行给定函数,进行过滤,将符合条件的数组项添加到新的数组中,并返回新的数组。
var aNum=[1,2,3,4];
console.log(aNum.filter(function (num) {
return num > 1;
}));//[2,3,4,]
aNum.filter(function (num) {
console.log(num > 1);//true true true
})
注意:filter()方法对数组项进行过滤,然后将符合条件的数组项添加到一个新的数组并返回,但是如果直接打印这个判断条件,相当于打印的判断条件的结果,只会返回true或者false。
1.find():返回第一个符合传入测试(函数)条件的数组元素。
var aNum=[10,20,30,40];
console.log(aNum.find(function (num) {
return num > 19;
}));//1
console.log(aNum.find(function (num) {
return num < 0;
}));//undefined
2.findIndex():返回符合传入测试(函数)条件的数组元素索引。
console.log(aNum.findIndex(function (num) { return num > 19; }));//3
3.includes():判断一个数组是否包含一个指定的值。
总结:
forEach()与map()是一对,用于数组遍历执行指定函数,前者不返回数组,后者返回 处理过的新数组。
every()与some()是一对,分别适用于检测数组是否全部满足某条件或者存在满足的数组项,返回true或false。
filter()则是相当于过滤器的存在,过滤掉数组中不符合条件的数据,将符合条件的数组项添加到新数组,并返回。
————————————————
版权声明:本文为CSDN博主「Mr_Han119」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_39155611/java/article/details/106294417
在 TypeScript 开发中,tsconfig.json 是个不可或缺的配置文件,它是我们在 TS 项目中最常见的配置文件,那么你真的了解这个文件吗?它里面都有哪些优秀配置?如何配置一个合理的 tsconfig.json 文件?本文将全面带大家一起详细了解 tsconfig.json 的各项配置。
本文将从以下几个方面全面介绍 tsconfig.json 文件:
了不起的 tsconfig.json 指南.png
水平有限,欢迎各位大佬指点~~
一、tsconfig.json 简介
1. 什么是 tsconfig.json
TypeScript 使用 tsconfig.json 文件作为其配置文件,当一个目录中存在 tsconfig.json 文件,则认为该目录为 TypeScript 项目的根目录。
通常 tsconfig.json 文件主要包含两部分内容:指定待编译文件和定义编译选项。
从《TypeScript编译器的配置文件的JSON模式》可知,目前 tsconfig.json 文件有以下几个顶层属性:
compileOnSave
compilerOptions
exclude
extends
files
include
references
typeAcquisition
文章后面会详细介绍一些常用属性配置。
2. 为什么使用 tsconfig.json
通常我们可以使用 tsc 命令来编译少量 TypeScript 文件:
/*
参数介绍:
--outFile // 编译后生成的文件名称
--target // 指定ECMAScript目标版本
--module // 指定生成哪个模块系统代码
index.ts // 源文件
*/
$ tsc --outFile leo.js --target es3 --module amd index.ts
但如果实际开发的项目,很少是只有单个文件,当我们需要编译整个项目时,就可以使用 tsconfig.json 文件,将需要使用到的配置都写进 tsconfig.json 文件,这样就不用每次编译都手动输入配置,另外也方便团队协作开发。
二、使用 tsconfig.json
目前使用 tsconfig.json 有2种操作:
1. 初始化 tsconfig.json
在初始化操作,也有 2 种方式:
手动在项目根目录(或其他)创建 tsconfig.json 文件并填写配置;
通过 tsc --init 初始化 tsconfig.json 文件。
2. 指定需要编译的目录
在不指定输入文件的情况下执行 tsc 命令,默认从当前目录开始编译,编译所有 .ts 文件,并且从当前目录开始查找 tsconfig.json 文件,并逐级向上级目录搜索。
$ tsc
另外也可以为 tsc 命令指定参数 --project 或 -p 指定需要编译的目录,该目录需要包含一个 tsconfig.json 文件,如:
/*
文件目录:
├─src/
│ ├─index.ts
│ └─tsconfig.json
├─package.json
*/
$ tsc --project src
注意,tsc 的命令行选项具有优先级,会覆盖 tsconfig.json 中的同名选项。
更多 tsc 编译选项,可查看《编译选项》章节。
三、使用示例
这个章节,我们将通过本地一个小项目 learnTsconfig 来学着实现一个简单配置。
当前开发环境:windows / node 10.15.1 / TypeScript3.9
1. 初始化 learnTsconfig 项目
执行下面命令:
$ mkdir learnTsconfig
$ cd .\learnTsconfig\
$ mkdir src
$ new-item index.ts
并且我们为 index.ts 文件写一些简单代码:
// 返回当前版本号
function getVersion(version:string = "1.0.0"): string{
return version;
}
console.log(getVersion("1.0.1"))
我们将获得这么一个目录结构:
└─src/
└─index.ts
2. 初始化 tsconfig.json 文件
在 learnTsconfig 根目录执行:
$ tsc --init
3. 修改 tsconfig.json 文件
我们设置几个常见配置项:
{
"compilerOptions": {
"target": "ES5", // 目标语言的版本
"module": "commonjs", // 指定生成代码的模板标准
"noImplicitAny": true, // 不允许隐式的 any 类型
"removeComments": true, // 删除注释
"preserveConstEnums": true, // 保留 const 和 enum 声明
"sourceMap": true // 生成目标文件的sourceMap文件
},
"files": [ // 指定待编译文件
"./src/index.ts"
]
}
其中需要注意一点:
files 配置项值是一个数组,用来指定了待编译文件,即入口文件。
当入口文件依赖其他文件时,不需要将被依赖文件也指定到 files 中,因为编译器会自动将所有的依赖文件归纳为编译对象,即 index.ts 依赖 user.ts 时,不需要在 files 中指定 user.ts , user.ts 会自动纳入待编译文件。
4. 执行编译
配置完成后,我们可以在命令行执行 tsc 命令,执行编译完成后,我们可以得到一个 index.js 文件和一个 index.js.map 文件,证明我们编译成功,其中 index.js 文件内容如下:
function getVersion(version) {
if (version === void 0) { version = "1.0.0"; }
return version;
}
console.log(getVersion("1.0.1"));
//# sourceMappingURL=index.js.map
可以看出,tsconfig.json 中的 removeComments 配置生效了,将我们添加的注释代码移除了。
到这一步,就完成了这个简单的示例,接下来会基于这个示例代码,讲解《七、常见配置示例》。
四、tsconfig.json 文件结构介绍
1. 按顶层属性分类
在 tsconfig.json 文件中按照顶层属性,分为以下几类:
tsconfig.json 文件结构(顶层属性).png
了不起的 tsconfig.json 指南.png
2. 按功能分类
tsconfig.json 文件结构(功能).png
五、tsconfig.json 配置介绍
1. compileOnSave
compileOnSave 属性作用是设置保存文件的时候自动编译,但需要编译器支持。
{
// ...
"compileOnSave": false,
}
2. compilerOptions
compilerOptions 属性作用是配置编译选项。
若 compilerOptions 属性被忽略,则编译器会使用默认值,可以查看《官方完整的编译选项列表》。
编译选项配置非常繁杂,有很多配置,这里只列出常用的配置。
{
// ...
"compilerOptions": {
"incremental": true, // TS编译器在第一次编译之后会生成一个存储编译信息的文件,第二次编译会在第一次的基础上进行增量编译,可以提高编译的速度
"tsBuildInfoFile": "./buildFile", // 增量编译文件的存储位置
"diagnostics": true, // 打印诊断信息
"target": "ES5", // 目标语言的版本
"module": "CommonJS", // 生成代码的模板标准
"outFile": "./app.js", // 将多个相互依赖的文件生成一个文件,可以用在AMD模块中,即开启时应设置"module": "AMD",
"lib": ["DOM", "ES2015", "ScriptHost", "ES2019.Array"], // TS需要引用的库,即声明文件,es5 默认引用dom、es5、scripthost,如需要使用es的高级版本特性,通常都需要配置,如es8的数组新特性需要引入"ES2019.Array",
"allowJS": true, // 允许编译器编译JS,JSX文件
"checkJs": true, // 允许在JS文件中报错,通常与allowJS一起使用
"outDir": "./dist", // 指定输出目录
"rootDir": "./", // 指定输出文件目录(用于输出),用于控制输出目录结构
"declaration": true, // 生成声明文件,开启后会自动生成声明文件
"declarationDir": "./file", // 指定生成声明文件存放目录
"emitDeclarationOnly": true, // 只生成声明文件,而不会生成js文件
"sourceMap": true, // 生成目标文件的sourceMap文件
"inlineSourceMap": true, // 生成目标文件的inline SourceMap,inline SourceMap会包含在生成的js文件中
"declarationMap": true, // 为声明文件生成sourceMap
"typeRoots": [], // 声明文件目录,默认时node_modules/@types
"types": [], // 加载的声明文件包
"removeComments":true, // 删除注释
"noEmit": true, // 不输出文件,即编译后不会生成任何js文件
"noEmitOnError": true, // 发送错误时不输出任何文件
"noEmitHelpers": true, // 不生成helper函数,减小体积,需要额外安装,常配合importHelpers一起使用
"importHelpers": true, // 通过tslib引入helper函数,文件必须是模块
"downlevelIteration": true, // 降级遍历器实现,如果目标源是es3/5,那么遍历器会有降级的实现
"strict": true, // 开启所有严格的类型检查
"alwaysStrict": true, // 在代码中注入'use strict'
"noImplicitAny": true, // 不允许隐式的any类型
"strictNullChecks": true, // 不允许把null、undefined赋值给其他类型的变量
"strictFunctionTypes": true, // 不允许函数参数双向协变
"strictPropertyInitialization": true, // 类的实例属性必须初始化
"strictBindCallApply": true, // 严格的bind/call/apply检查
"noImplicitThis": true, // 不允许this有隐式的any类型
"noUnusedLocals": true, // 检查只声明、未使用的局部变量(只提示不报错)
"noUnusedParameters": true, // 检查未使用的函数参数(只提示不报错)
"noFallthroughCasesInSwitch": true, // 防止switch语句贯穿(即如果没有break语句后面不会执行)
"noImplicitReturns": true, //每个分支都会有返回值
"esModuleInterop": true, // 允许export=导出,由import from 导入
"allowUmdGlobalAccess": true, // 允许在模块中全局变量的方式访问umd模块
"moduleResolution": "node", // 模块解析策略,ts默认用node的解析策略,即相对的方式导入
"baseUrl": "./", // 解析非相对模块的基地址,默认是当前目录
"paths": { // 路径映射,相对于baseUrl
// 如使用jq时不想使用默认版本,而需要手动指定版本,可进行如下配置
"jquery": ["node_modules/jquery/dist/jquery.min.js"]
},
"rootDirs": ["src","out"], // 将多个目录放在一个虚拟目录下,用于运行时,即编译后引入文件的位置可能发生变化,这也设置可以虚拟src和out在同一个目录下,不用再去改变路径也不会报错
"listEmittedFiles": true, // 打印输出文件
"listFiles": true// 打印编译的文件(包括引用的声明文件)
}
}
3. exclude
exclude 属性作用是指定编译器需要排除的文件或文件夹。
默认排除 node_modules 文件夹下文件。
{
// ...
"exclude": [
"src/lib" // 排除src目录下的lib文件夹下的文件不会编译
]
}
和 include 属性一样,支持 glob 通配符:
* 匹配0或多个字符(不包括目录分隔符)
? 匹配一个任意字符(不包括目录分隔符)
**/ 递归匹配任意子目录
4. extends
extends 属性作用是引入其他配置文件,继承配置。
默认包含当前目录和子目录下所有 TypeScript 文件。
{
// ...
// 把基础配置抽离成tsconfig.base.json文件,然后引入
"extends": "./tsconfig.base.json"
}
5. files
files 属性作用是指定需要编译的单个文件列表。
默认包含当前目录和子目录下所有 TypeScript 文件。
{
// ...
"files": [
// 指定编译文件是src目录下的leo.ts文件
"scr/leo.ts"
]
}
6. include
include 属性作用是指定编译需要编译的文件或目录。
{
// ...
"include": [
// "scr" // 会编译src目录下的所有文件,包括子目录
// "scr/*" // 只会编译scr一级目录下的文件
"scr/*/*" // 只会编译scr二级目录下的文件
]
}
7. references
references 属性作用是指定工程引用依赖。
在项目开发中,有时候我们为了方便将前端项目和后端node项目放在同一个目录下开发,两个项目依赖同一个配置文件和通用文件,但我们希望前后端项目进行灵活的分别打包,那么我们可以进行如下配置:
{
// ...
"references": [ // 指定依赖的工程
{"path": "./common"}
]
}
8. typeAcquisition
typeAcquisition 属性作用是设置自动引入库类型定义文件(.d.ts)相关。
包含 3 个子属性:
enable : 布尔类型,是否开启自动引入库类型定义文件(.d.ts),默认为 false;
include : 数组类型,允许自动引入的库名,如:["jquery", "lodash"];
exculde : 数组类型,排除的库名。
{
// ...
"typeAcquisition": {
"enable": false,
"exclude": ["jquery"],
"include": ["jest"]
}
}
六、常见配置示例
本部分内容中,我们找了几个实际开发中比较常见的配置,当然,还有很多配置需要自己摸索哟~~
1. 移除代码中注释
tsconfig.json:
{
"compilerOptions": {
"removeComments": true,
}
}
编译前:
// 返回当前版本号
function getVersion(version:string = "1.0.0"): string{
return version;
}
console.log(getVersion("1.0.1"))
编译结果:
function getVersion(version) {
if (version === void 0) { version = "1.0.0"; }
return version;
}
console.log(getVersion("1.0.1"));
2. 开启null、undefined检测
tsconfig.json:
{
"compilerOptions": {
"strictNullChecks": true
},
}
修改 index.ts 文件内容:
const leo;
leo = new Pingan('leo','hello');
这时候编辑器也会提示错误信息,执行 tsc 后,控制台报错:
src/index.ts:9:11 - error TS2304: Cannot find name 'Pingan'.
9 leo = new Pingan('leo','hello');
Found 1 error.
3. 配置复用
通过 extends 属性实现配置复用,即一个配置文件可以继承另一个文件的配置属性。
比如,建立一个基础的配置文件 configs/base.json :
{
"compilerOptions": {
"noImplicitAny": true,
"strictNullChecks": true
}
}
在tsconfig.json 就可以引用这个文件的配置了:
{
"extends": "./configs/base",
"files": [
"main.ts",
"supplemental.ts"
]
}
4. 生成枚举的映射代码
在默认情况下,使用 const 修饰符后,枚举不会生成映射代码。
如下,我们可以看出:使用 const 修饰符后,编译器不会生成任何 RequestMethod 枚举的任何映射代码,在其他地方使用时,内联每个成员的值,节省很大开销。
const enum RequestMethod {
Get,
Post,
Put,
Delete
}
let methods = [
RequestMethod.Get,
RequestMethod.Post
]
编译结果:
"use strict";
let methods = [
0 /* Get */,
1 /* Post */
];
当然,我们希望生成映射代码时,也可以设置 tsconfig.json 中的配置,设置 preserveConstEnums 编译器选项为 true :
{
"compilerOptions": {
"target": "es5",
"preserveConstEnums": true
}
}
最后编译结果变成:
"use strict";
var RequestMethod;
(function (RequestMethod) {
RequestMethod[RequestMethod["Get"] = 0] = "Get";
RequestMethod[RequestMethod["Post"] = 1] = "Post";
RequestMethod[RequestMethod["Put"] = 2] = "Put";
RequestMethod[RequestMethod["Delete"] = 3] = "Delete";
})(RequestMethod || (RequestMethod = {}));
let methods = [
0 /* Get */,
1 /* Post */
];
5. 关闭 this 类型注解提示
通过下面代码编译后会报错:
const button = document.querySelector("button");
button?.addEventListener("click", handleClick);
function handleClick(this) {
console.log("Clicked!");
this.removeEventListener("click", handleClick);
}
报错内容:
src/index.ts:10:22 - error TS7006: Parameter 'this' implicitly has an 'any' type.
10 function handleClick(this) {
Found 1 error.
这是因为 this 隐式具有 any 类型,如果没有指定类型注解,编译器会提示“"this" 隐式具有类型 "any",因为它没有类型注释。”。
解决方法有2种:
指定 this 类型,如本代码中为 HTMLElement 类型:
HTMLElement 接口表示所有的 HTML 元素。一些HTML元素直接实现了 HTMLElement 接口,其它的间接实现HTMLElement接口。
关于 HTMLElement 可查看详细。
使用 --noImplicitThis 配置项:
在 TS2.0 还增加一个新的编译选项: --noImplicitThis,表示当 this 表达式值为 any 类型时生成一个错误信息。我们设置为 true 后就能正常编译。
{
"compilerOptions": {
"noImplicitThis": true
}
}
七、Webpack/React 中使用示例
1. 配置编译 ES6 代码,JSX 文件
创建测试项目 webpack-demo,结构如下:
webpack-demo/
|- package.json
|- tsconfig.json
|- webpack.config.js
|- /dist
|- bundle.js
|- index.html
|- /src
|- index.js
|- index.ts
|- /node_modules
安装 TypeScript 和 ts-loader:
$ npm install --save-dev typescript ts-loader
配置 tsconfig.json,支持 JSX,并将 TypeScript 编译为 ES5:
{
"compilerOptions": {
"outDir": "./dist/",
"noImplicitAny": true,
+ "module": "es6",
+ "target": "es5",
+ "jsx": "react",
"allowJs": true
}
}
还需要配置 webpack.config.js,使其能够处理 TypeScript 代码,这里主要在 rules 中添加 ts-loader :
const path = require('path');
module.exports = {
entry: './src/index.ts',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/
}
]
},
resolve: {
extensions: [ '.tsx', '.ts', '.js' ]
},
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
}
};
2. 配置 source map
想要启用 source map,我们必须配置 TypeScript,以将内联的 source map 输出到编译后的 JavaScript 文件中。
只需要在 tsconfig.json 中配置 sourceMap 属性:
{
"compilerOptions": {
"outDir": "./dist/",
+ "sourceMap": true,
"noImplicitAny": true,
"module": "commonjs",
"target": "es5",
"jsx": "react",
"allowJs": true
}
}
然后配置 webpack.config.js 文件,让 webpack 提取 source map,并内联到最终的 bundle 中:
const path = require('path');
module.exports = {
entry: './src/index.ts',
+ devtool: 'inline-source-map',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/
}
]
},
resolve: {
extensions: [ '.tsx', '.ts', '.js' ]
},
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
}
};
八、总结
本文较全面介绍了 tsconfig.json 文件的知识,从“什么是 tsconfig.js 文件”开始,一步步带领大家全面认识 tsconfig.json 文件。
文中通过一个简单 learnTsconfig 项目,让大家知道项目中如何使用 tsconfig.json 文件。在后续文章中,我们将这么多的配置项进行分类学习。最后通过几个常见配置示例,解决我们开发中遇到的几个常见问题。
路由设计
本则路由考虑验证进入登录页面,完成登录操作进入首页。
import Vue from "vue";
import Router from "vue-router";
Vue.use(Router);
import store from "@/store/store";
// (延迟加载)
const Login = () => import("@/views/login");
const Home = () => import("@/views/home");
const HomeRoute = {
path: "/",
name: "首页",
component: Home
};
export { HomeRoute };
const router = new Router({
base: process.env.BASE_URL,
routes: [
{
path: "/login",
name: "登录",
component: Login
},
HomeRoute
]
});
router.beforeEach((to, from, next) => {
let loginName = store.state.user.loginName;
if (to.path === "/" && loginName == "") {
next("/login");
} else {
next();
}
});
export default router;
数据模型
const state = {
loginName: ""
};
const mutations = {
SET_LOGINNAME(state, loginName) {
state.loginName = loginName;
}
};
const actions = {
login({ commit }, userInfo) {
return new Promise((res, ret) => {
commit("SET_LOGINNAME", userInfo);
res();
});
},
logout({ commit }) {
return new Promise((res, ret) => {
commit("SET_LOGINNAME", "");
res();
});
}
};
export default {
namespaced: true,
state,
mutations,
actions
};
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
import user from "./modules/user";
const store = new Vuex.Store({
modules: {
user
}
});
export default store;
组件
<div class="modify">
<input
type="text"
@keydown.enter.prevent="handleKeydown"
v-model="currentVal"
placeholder="使用enter键切换频道"
/>
<button @click="reset" style="margin-left:5px;outline:none;cursor:pointer;">复位</button>
</div>
import { mapState, mapMutations, mapActions } from "vuex";
export default {
name: "login",
data() {
return {
currentVal: "",
list: ["咨询服务", "音悦台", "体育台", "财经频道", "时尚资讯"],
index: 0
};
},
computed: {
...mapState({
loginName: state => state.user.loginName
})
},
methods: {
...mapActions({
login: "user/login"
}),
handleToHome() {
let userInfo = "user";
this.login(userInfo);
this.$router.push({
path: "/"
});
},
前言
在平时H5或者RN开发时,我们业务场景中大部分都不是单页面的需求,那这时我们就能使用路由在进行多页面的切换。下面会对比一下react路由和RN路由的本质区别和使用方法。
路由(routing)是指分组从源到目的地时,决定端到端路径的网络范围的进程
React路由
简介
使用React构建的单页面应用,要想实现页面间的跳转,首先想到的就是使用路由。在React中,常用的有两个包可以实现这个需求,那就是react-router和react-router-dom。本文主要针对react-router-dom进行说明
在根组件上配置路由,引用react-router-dom结构{ HashRouter as Router, Route ,Link ,Redirect ,Switch },HashRouter组件是路由的根组件,定义属性和方法传递给子组件。Router组件进行路由,指定每个路由跳转到相应的组件。Link组件指定跳转链接。Redirect组件路由重定向,不管什么情况下,都会跳转当前指定的路径,和switch组件联合起来一起调用,当路径匹配到路由,不在往下匹配
两类路由
HashRouter:利用监听hash变化(有一个事件hashchange)实现路由切换,它是路由容器,
渲染子组件,并向下层子组件传递(Context上下文传递)loaction,history等路由信息
BrowserHistory:利用H5Api实现路由切换,是路由容器,渲染子组件,
并向子组件传递loaction,history等路由信息
路由配置
image-20200601110809995
路由实现原理
HashRouter只是一个容器,本身并没有DOM结构
它渲染的就是它的子组件,并向下层传递location
组件挂载完成之后根据hash改变pathname的值,如果没有hash值就默认展示根组件
需要跳转路由页面时我们使用link或者push去赋值hash的pathname 如this.props.history.push({ pathname: preview, param: { pic, index } });
当hash值发生变化的时候会通过hashchange捕获变化,并给pathname重新赋值
拿到上下文中传过来的location,然后取出pathname。再对它的子组件进行遍历,如果子组件的path属性和当前上下文中传过来的pathname属性相匹配就进行渲染,若不匹配就返回null。
总结
React路由是实质就是,根据遍历识别路由的pathname,来切换router路由容器中component组件的加载渲染。每次更改pathname就都是组件的重新渲染流程,页面也都会呈现出刷新的效果。
RN路由
简介
RN把导航和路由都集中到了react-navigation库里面
组件使用堆栈式的页面导航来实现各个页面跳转
构造函数:StackNavigator(RouteConfigs, StackNavigatorConfig)
RouteConfigs:页面路由配置
StackNavigatorConfig:路由参数配置
路由配置
image-20200601111333107
参数详解
navigationOptions:配置StackNavigator的一些属性。
title:标题,如果设置了这个导航栏和标签栏的title就会变成一样的,不推荐使用
header:可以设置一些导航的属性,如果隐藏顶部导航栏只要将这个属性设置为null
headerTitle:设置导航栏标题,推荐
headerBackTitle:设置跳转页面左侧返回箭头后面的文字,默认是上一个页面的标题。可以自定义,也可以设置为null
headerTruncatedBackTitle:设置当上个页面标题不符合返回箭头后的文字时,默认改成"返回"
headerRight:设置导航条右侧。可以是按钮或者其他视图控件
headerLeft:设置导航条左侧。可以是按钮或者其他视图控件
headerStyle:设置导航条的样式。背景色,宽高等
headerTitleStyle:设置导航栏文字样式
headerBackTitleStyle:设置导航栏‘返回’文字样式
headerTintColor:设置导航栏颜色
headerPressColorAndroid:安卓独有的设置颜色纹理,需要安卓版本大于5.0
gesturesEnabled:是否支持滑动返回手势,iOS默认支持,安卓默认关闭
screen:对应界面名称,需要填入import之后的页面
mode:定义跳转风格
card:使用iOS和安卓默认的风格
modal:iOS独有的使屏幕从底部画出。类似iOS的present效果
headerMode:返回上级页面时动画效果
float:iOS默认的效果
screen:滑动过程中,整个页面都会返回
none:无动画
cardStyle:自定义设置跳转效果
transitionConfig: 自定义设置滑动返回的配置
onTransitionStart:当转换动画即将开始时被调用的功能
onTransitionEnd:当转换动画完成,将被调用的功能
path:路由中设置的路径的覆盖映射配置
initialRouteName:设置默认的页面组件,必须是上面已注册的页面组件
initialRouteParams:初始路由参数
路由首页
react:
image-20200601111638902
在react中初始化时没有指定hash值,route会匹配路由表里面的根组件”/”
RN:
image-20200601111722749
RN 需要在StackNavigatorConfig里面指定首页
RN路由使用
image-20200601112012191
在入口路由列表注册完成之后 在导航器中的每一个页面,都有 navigation 属性 通过提供的navigate方法来提供跳转
navigation
在导航器中每一个页面都有navigation属性,该属性有以下几个属性/方法
navigate 跳转到其他页面 常用参数如下
routeName 导航器中配置的路由名称
params 传递到下一个页面的参数
state:state 里面包含有传递过来的参数 params 、 key 、路由名称 routeName
setParams 更改当前页面路由参数(后面详细介绍)
goBack: 回退可穿参数
navigate
setParams
关于循环设计,可持续发展是商业领域非常关注的话题,作为UX需提前转变思维,给企业带来更多价值,一线大厂已在运用这种思维
本文共 3589 字,预计阅读 10 分钟
译者推荐|本文从“可持续”和“设计”的两点谈起,来论述从线性经济向可持续经济的转变,以及“可持续设计”的四个主要阶段:理解、定义、制造、发布。
“循环设计”不是为了追求时髦或者抬升设计地位,而是将这个已经日益庸俗化的“设计”冠为自己的定语,是设计本身发展所趋,以及可持续发展所需,设计界需要对自己的责任有所承担,形成一个全局观、系统性看待设计问题的方式。让回收利用和可持续发展成为一种规范,从而做到一劳永逸。
我们生活在一个呼唤变革的世界。在过去的50年中,现代社会所依赖的漫不经心和无休止的消费是不可持续的。我们从小就不关心自己的事情。如果有什么东西坏了,我们也就不修了。产品被设计成用完直接丢弃,而不是去修复。数字产品也不例外。然而,为了解决这些问题,出现了一种新的思维方式:循环设计(可持续设计)①。(益达说:其实这个理念和风格已经存在了很长的时间,大多应用在不为大众所知的能源、材料再生流程之中,然而随着时代的发展,循环设计可以变得更加普世。)
①注:循环设计是20世纪80-90年代产生的一种设计风格,他又称回收设计,是指实现广义收回和利用的方法,即在进行产品设计时,充分考虑产品零部件及材料回收的可能性,回收价值的大致方法,回收处理结构工艺性等于与回收有关的一系列问题,以达到零部件及材料资源和能源的再利用。它旨在通过设计来节约能源和材料,减少对环境的污染,使人类的设计物能多次反复利用,形成产品设计和使用的良性循环。
那么,循环设计方法意味着什么?在数字产品上要如何使用?在回答这些问题之前,首先,我们需要仔细观察我们是如何构建我们的世界,为什么这个世界已经不可持续了,并且要理解环保世界的需求是如何改变我们的思维方式,促使我们渴望从线性设计模型转变为循环设计模型。
向循环转变
我们的经济主要基于“按需配置”流程之上。在此线性系统中,我们构建了会在一段时间后淘汰的产品,并且将其组件视为垃圾。与此相反,循环设计方法将产品的生命周期视为一个闭环,其中资源不断地被重新利用。
在“经典”线性模型中,产品经历了生产、消费和破坏的各个阶段,最终以浪费告终。在设计一款循环产品过程中,我们使用的方法包含四大阶段,这四个阶段形成了一个闭环,并形成了一个恒定的循环,在这个循环中,不仅仅只有闪闪发亮的、新的,未使用过的材料才被受欢迎。
循环设计方法的四个阶段是:
理解 / 定义 / 制造 / 发布
当我们同时看线性设计和循环设计模型方法时,有一点吸引人的是,开始设计东西的时候,方法的差异。从只是生产某种东西,到对我们将要生产的产品做出深思熟虑的决定,以及在实施过程中付出的努力和关心,这是一个大转变。
看看我们现在的立场
为什么做出这种转变如此的重要?我确信每个看新闻的人都听说过气候变化。NASA 致力于解决环境问题,因此我们都可以非常详细地了解人类行为和无限增长对我们生态系统的影响。
但好消息是我们不必继续这样做,因为我们可以很容易从数字世界中“产生”方式中学习事物的产生。电力废弃物已成为现代世界的主要废弃物来源之一。大量的手机和电脑被扔掉,随之经济是建立在每年都有新东西的基础上的。
当您的手机屏幕意外碎裂时,我们该怎么办?我们知道如何处理它吗?我们知道如何修理吗?我们并不知道……但是幸运的是,有些设计师对此问题提出了解决方案。Fairphone② 是一种合乎情理,模块化的智能手机,其组件数量很少,可以轻松更换和回收。大公司也应朝这个方向迈出一步,让回收利用和可持续发展成为一种时尚和规范,一劳永逸。
② Fairphone:这家公司生产的手机希望实现全球手机供应链的公平贸易,具体而言就是不使用“冲突矿物”并且确保生产手机的工人没有被奴役和压榨,目前仍然坚持在非洲贫困和战乱的国家进口材料,已经在刚果和卢旺达境内找到了一些矿山,用更好的商业实践推动当地经济更健康地发展。
设计和设计师的重要性
设计师,比任何其他专业人士,都更有可能在一转变中产生巨大的影响的人。我还敢说,我们有责任采用可持续设计的方式行动和思考。因为是我们创造了那些最终出现在传送带上的东西。我们也有责任教育我们的用户。幸运的是,越来越多的人重视具有可持续发展目标的产品或品牌,或者重视起在产品背后有意义的故事。同样,可持续发展不仅成为流行语,而且成为一种价值观,被越来越多的人意识到基于有限资源的无限增长是无法实现的目标。但是,要从线性经济向可持续经济转变,我们需要学习不同的思维方式。幸运的是,智能设备和数字产品的时代带来了一种复杂的设计思维方法,可以作为物理世界中生产链的范例。
用户体验必须提供什么
地球上有一个地方,您不能随便丢东西:互联网。这是一个对已有产品进行再构思的地方,您只能去完善它,不能丢弃它,因为如果您一夜之间说:“我不喜欢我的网站,明天我将推出一个全新的网站”,那您便会失去用户。
如果我们看一下可持续发展设计方法的四个主要阶段,就会发现我们在用户体验设计中使用的方法与此很相似。
让我们再次看一下四个阶段,然后将其更详细地分解:
当我们谈论与循环设计相关的理解时,我们谈论的是在开始设计一个未来的产品之前就了解它的用户和环境。研究一直是数字产品设计的基础。与数字产品的连接比与实体产品的连接要更多的涉及到人类的心理。因此不可避免地要开发出新的研究方法,以帮助我们洞察用户在使用某种产品时的想法、感受和行为。但这不仅与用户有关, 研究还必须深入到经济领域,并研究未来产品的组成部分,同时牢记它们必须可被再次利用。
在此阶段,将定义(商业)目标,并构建一个商业模型画布作为生产过程的计划。用户体验使用这种方法已有一段时间了,让涉众参与其中,并在设计过程中更多地激活它们。为我们设计的产品设定一个目标是至关重要的,因为有了它,我们可以为用户创造额外的价值。因此,无论是制作商业模型画布还是举办精彩的价值主张研讨会,在生产方式中实施这些方法都会对当前的生产流程产生巨大的影响。
这是关键部分。现在我们正在做的事情就好像没有明天一样。随着每种无法回收的产品的出现,我们产生的废料越来越多。但是循环方法是为产品创建一个原型,并定义将需要使用那些材料反映在产品原型上,并在定义阶段概述的商业模型上定义材料。原型设计和构思是用户体验设计过程中的关键要素,这也是为什么需要制作原型。
根据循环设计模型,随着产品的发布,生产周期进入了第四阶段,然同时理解阶段又重新开始了。对于数字产品来说,这是自然发生的事前:你发布一个产品,基于该版本收集反馈,然后构思它,周而复始,这个循环再次产生。
但是,观察这个循环并建立这些连接仅仅是冰山一角。在数字时代发展起来的设计思维给世界带来了许多反思。
变革中的大佬
幸运的是,已经有许多大品牌意识到转变的必要性,并采取和提出了数字设计思维方法来支持转变,并建立循环设计的时代。根据《循环设计指南》,“我们应该把我们设计的所有东西都看作软件产品和服务——这些产品和服务可以基于我们通过反馈得到的数据而不断的发。”
用户体验研究和用户体验设计一直是在做的一件事是:基于全面的研究和真实的用户需求来构建产品。上面的设计指南是非常复杂的工具,具有许多可能的方法。它强调了从产品到服务流程转变的重要性,并展示如何使用敏捷流程并将其实施到构建产品的方法之中。
IDEO(全球顶尖的设计咨询公司)与 Ellen Macarthur Foundation(艾伦·麦克阿瑟基金会)合作,试图“试图通过设计构建一个具有恢复性和再生性的经济框架”。在这里,您可以找到几乎每个生产方面和领域——例如食品、时装、经济和设计——并在每个领域中提出解决方案,以打破线性生产系统。
耐克还宣布了其基于循环设计模型生产高品质运动鞋的新方法原则。正如您已经看到的那样,无论您身处哪个经济领域,都可以为循环生产过程的蓬勃发展做贡献,并成为一支主导力量。
重要的结论
我认为,作为设计师,我们始终要为变革而努力,并始终努力与客户、产品或服务保持紧密的关系,并通过构思使其不断完善,以实现这一目标。这是因为伟大的事情只有通过时间和不断的反思才能实现。在离线世界中,数字设计过程也有很多东西可以贡献。希望通过教育,能有更多的大公司意识到用户真正想要的产品是具有更多功能并可持续使用的,而不仅仅是将它们当作一次性产品,一旦它们不像最初那样光鲜就把她扔掉。
转自:站酷-大猴儿er
(Onboarding 是指用户第一次使用产品时认识、熟悉产品的过程)
往期回顾:
对设计系统有所认识的话应该会知道原子设计(Atomic Design)的重要性,我们也能将同样的概念应用在 onboarding 上,其构成从宏观到微观分成体验流程、控件形式与界面元素三个层级。
体验流程是一个有时序性的旅程,可以由数个不同的载具表现组合而成;控件则是承载信息而存在的平面,可以放上不同的元素;而界面元素是无法再行分割的对象。
我在每个阶段都举了几个常见的例子,搭配市面上产品的应用方法。
一个产品通常不会只有一种用户——使用健身 app 可能是为了减肥,也可能是为了增重;使用协作产品可能是为了记录工作成果,也可能是为了管理团队。如果能够在 onboarding 阶段了解用户的主要意图、在适量的搜集信息后将用户分流(记得上篇的避免过度吸收法则吗?),就能够打造更切身的体验。
除了用户分流之外,还有一些概括性的分流如下:
真正新的使用者 vs. 回流使用者
某些使用者只是因为一些外部因素(手机掉了、手滑删掉 app、忘记密码)而重新登录/下载产品,他们已经接触过产品的核心价值了,并不需要再次学习,这就是为什么跳过(skip)功能很重要。
邀请人 vs. 受邀人
如果邀请人已经设定好群组,受邀人应该自动被加入,某些信息也该自动填入,而非让用户再填一次,从而导致出错。
新手 vs. 老手
与专业领域高度相关的产品(例如 Adobe 系列、CAD 系列等)还可以将用户区分为已经很熟悉作业流程的老手与初学者等级的新手。老手最重视的是定制化以符合他们习惯的作业流程、作业效率高不高,并且跟其他竞品做比较。新手则不然,初次使用产品时的他们也是初次进入这个领域,如果能帮助他们更加了解这个领域的大致流程的话会很加分。
△ Photoshop 的丰富资源指引(Rich Tooltips)对于新手来说是一大神助
特别点出几个重点 features,简单地告知用户最重要的功能为何、组件在哪里,用户在登录产品之后只要知道这几个主要动作就可以打遍天下无敌手。
△ Slack 指出 channel 和对话框如何使用
当产品较为复杂,难以指出特定 feature 时,也常以影片或图片来展现产品价值——也就是画一张大饼给用户,让他们想象未来的生活在用了这个产品后会有多便利,或是让自我感觉提升。
相对于展示,实际演练更着重用户要亲自执行。许多研究都证实从做中学习的成效,就算只是训练肌肉记忆(muscle memory),也能让用户对界面的物理空间更有概念,像是「噢对刚才有做过,我记得按键在右上角」。
我们也可以设计一套 demo 版的试用,像是将 scenario 抓一个出来让用户试跑,跑过一个假想的故事情节后印象会更为深刻。
在初次展示后将用户引导的数据回收再利用,变成每当用户有问题时都随时可用的资源库。
1. 导览 Guided Tour
可能是影片,也可能是滑动式 slideshow,但总之是向用户介绍产品的主要功能或是传达产品价值,通常是为了展示的体验流程所设置。
2. 指引 Tooltip / Coachmark
用极小的空间指向目标物,既可以集中用户注意力,又不会遮盖住大部分的使用空间,用户可以一边进行正规作业,一边通过指引了解不懂的内容。
△ Dropbox Paper 用诙谐的语气鼓励你开始打字
有一阵子很多产品会将所有指引放在同一张图上,但其实使用不当很容易造成信息过载、注意力分散、之后会很难全数记住的情况,我的建议是一次一个比较好。
3. 秀给我看 Show Me
通常只会出现在教程中,强迫用户一定要亲自去按到按键或执行关键动作,切实练习用户的肌肉记忆。
1. 空白状态的行动呼吁 Empty State CTA
擅用空白状态是 onboarding 重要的一环,毕竟许多产品在用户动作之前可能都没有太多料,这时候就要运用行动呼吁。
例如 Tumblr 在指导使用者选择有兴趣的领域之后就指出如何 po 内容,因为其用户生成平台(UGC,User-generated content)的本质就是要鼓励用户多交流、多产出,平台才有价值。
2. 进度列 Progress Bar
提供进度可视化,让用户有掌控时间的感觉,而不是不知道自己还要再走几个步骤而感到不耐烦。
Basecamp 将进度列摆在上端,让使用者知道已经快做完这些设置了
3. 待办事项 Checklist
人类天生喜欢将事情「全部做完」,欲罢不能:科技如何让我们上瘾?可以协助我们「引诱」用户更愿意完成 onboarding 程序。
Bluma Zeigarnik 让受试者完成某些任务,但在他们完成另一些任务前打断他们,强迫他们开始进行其他的任务。这些受试者会非常不情愿地停下手上正在做的事,有些人会强烈抗议,有些人甚至会生气,这显示出 Zeigarnik 的打断为他们带来多么大的压力。到实验的最后,受试者清楚记得那些未完成的任务。如果是打断后一阵子又让他们完成的话,就没有这种效应。(摘自 欲罢不能)
4. 跳过 Skip
每次有 onboarding 都会选择跳过的人举手!
我喜欢把这称为不喜欢看桌游规则的人们,所以在使用中遇到困难时给予提示,对他们来说才是最实用而且最愉悦的,而不是在使用前的纸上谈兵。
△ Tumblr 在使用者第一次发文时才提示如何装饰文字
设计 onboarding 时并不是只能选择一种方法,我们可以灵活运用各种元素。将 onboarding 视为一个旅程,而不是单一元素的无限重复。我看过大部分最棒的例子都是综合使用上述多种元素,以下以新兴生产力工具 Coda 为例,来看看集上述大成的 onboarding 作品。
在第一次进入产品使用引导时,可以自行选择偏好的学习方式——影片或是交互式教学。
Coda 并没有强制用户立刻进入 onboarding 模式,而是在呈现主画面之余,让我们看到右侧的待办事项,令人产生「想将之完成」的欲望。
点入后,先有个 setup 内容,任务情境是为了项目经理所设计,但随着使用教程的进行,用户也能够联想到自己生活中的其他任务,例如安排家庭旅游、写系列文案、追踪买家信息等。
正式进入学习阶段后,进度条就出现了。
单纯根据文字叙述,用户仍然可能混淆,这时候 Show Me 功能可以减少不必要的误解。
同上,当用专有名词(此处的 section )介绍某个界面元素时,将其他无关紧要的区域遮盖住,聚焦在重点区域,用户更容易将专有名词跟界面链接在一起。否则单说 section 谁知道是哪一个 section?
结束时记得给辛苦学习的用户一些奖励,并且贴心附上下一步,当然还是要留给使用者最终决定权。
完成一项后,Coda 会帮用户将完成的项目划除,于是得到立即的回馈。
完成所有步骤之后,原先是教程列的右侧区域转变成资源列,每当使用上遇到困难时就可以寻求各种协助。
Onboarding 并不是只会出现一次,推出多年的产品也仍会时常进行。
onboarding 的程序,例如推出新 feature 或有重新设计(redesign)的时候,目的仍然是让用户快速熟悉产品,所以这是身为产品设计师不可忽视的一环。
另外,除了 UI/UX 设计之外,文案写作也极其重要——如何跟用户诉说一个吸引人的故事、描绘出他们想象中的自己,也是成功 onboarding 必要元素。
文章来源:优设 作者:
原始transition组件和CSS
定义transition的最简单方法是使用transition·或transition-group 组件。这需要为transition定义一个name`和一些CSS。
<template>
<div id="app">
<button v-on:click="show = !show">
Toggle
</button>
<transition name="fade">
<p v-if="show">hello</p>
</transition>
</div>
</template>
<script>
export default {
name: "App",
data() {
return {
show: true
};
}
};
</script>
<style>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
</style>
图片描述
看起来容易,对吧?然而,这种方法有一个问题。我们不能在另一个项目中真正重用这个transition。
封装transition组件
如果我们将前面的逻辑封装到一个组件中,并将其用作一个组件,结果会怎样呢?
// FadeTransition.vue
<template>
<transition name="fade">
<slot></slot>
</transition>
</template>
<script>
export default {
};
</script>
<style>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
</style>
// App.vue
<template>
<div id="app">
<button v-on:click="show = !show">
Toggle transition
</button>
<fade-transition>
<div v-if="show" class="box"></div>
</fade-transition>
</div>
</template>
<script>...</script>
<style>...</style>
图片描述
通过在transition组件中提供一个slot,我们几乎可以像使用基本transition组件一样使用它。这比前面的例子稍微好一点,但是如果我们想要传递其他特定于transition的prop,比如mode或者一些hook,该怎么办呢
封装的包装器transition组件
幸运的是,Vue 中有一个功能,使我们可以将用户指定的所有额外props和监听器传递给我们的内部标签/组件。 如果你还不知道,则可以通过$attrs访问额外传递的 props,并将它们与v-bind结合使用以将它们绑定为props。 这同样适用于通过$listeners进行的事件,并通过v-on对其进行应用。
// FadeTransition.vue
<template>
<transition name="fade" v-bind="$attrs" v-on="$listeners">
<slot></slot>
</transition>
</template>
<script>
export default {};
</script>
<style>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
</style>
// App.vue
...
<fade-transition mode="out-in">
<div key="blue" v-if="show" class="box"></div>
<div key="red" v-else class="red-box"></div>
</fade-transition>
...
图片描述
完整事例地址:https://codesandbox.io/s/yjl1...
现在,我们可以传递普通transition组件可以接受的任何事件和支持,这使得我们的组件更加可重用。但为什么不更进一步,增加通过 prop 轻松定制持续时间的可能性。
显式持续时间 prop
Vue 为transition组件提供了一个duration prop,然而,它是为更复杂的动画链接而设计的,它帮助 Vue 正确地将它们链接在一起。
在我们的案例中,我们真正需要的是通过组件prop控制CSS animation/transition。 我们可以通过不在CSS中指定显式的CSS动画持续时间,而是将其作为样式来实现。 我们可以借助transition hook来做到这一点,该transition hook与组件生命周期 hook 非常相似,但是它们在过渡所需元素之前和之后被调用。 让我们看看效果如何。
// FadeTransition.vue
<template>
<transition name="fade"
enter-active-class="fadeIn"
leave-active-class="fadeOut"
v-bind="$attrs"
v-on="hooks">
<slot></slot>
</transition>
</template>
<script>
export default {
props: {
duration: {
type: Number,
default: 300
}
},
computed: {
hooks() {
return {
beforeEnter: this.setDuration,
afterEnter: this.cleanUpDuration,
beforeLeave: this.setDuration,
afterLeave: this.cleanUpDuration,
...this.$listeners
};
}
},
methods: {
setDuration(el) {
el.style.animationDuration = `${this.duration}ms`;
},
cleanUpDuration(el) {
el.style.animationDuration = "";
}
}
};
</script>
<style>
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.fadeIn {
animation-name: fadeIn;
}
@keyframes fadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
.fadeOut {
animation-name: fadeOut;
}
</style>
图片描述
完整事例地址:https://codesandbox.io/s/j4qn...
现在,我们可以控制实际的可见过渡时间,这使我们可重用的过渡变得灵活且易于使用。 但是,如何过渡多个元素(如列表项)呢?
Transition group 支持
你想到的最直接的方法可能是创建一个新组件,比如fade-transition-group,然后将当前transition标签替换为transition-group标签,以实现 group transition。如果我们可以在相同的组件中这样做,并公开一个将切换到transition-group实现的group prop,那会怎么样呢?幸运的是,我们可以通过render函数或component和is属性来实现这一点。
// FadeTransition.vue
<template>
<component :is="type"
:tag="tag"
enter-active-class="fadeIn"
leave-active-class="fadeOut"
move-class="fade-move"
v-bind="$attrs"
v-on="hooks">
<slot></slot>
</component>
</template>
<script>
export default {
props: {
duration: {
type: Number,
default: 300
},
group: {
type: Boolean,
default: false
},
tag: {
type: String,
default: "div"
}
},
computed: {
type() {
return this.group ? "transition-group" : "transition";
},
hooks() {
return {
beforeEnter: this.setDuration,
afterEnter: this.cleanUpDuration,
beforeLeave: this.setDuration,
afterLeave: this.cleanUpDuration,
leave: this.setAbsolutePosition,
...this.$listeners
};
}
},
methods: {
setDuration(el) {
el.style.animationDuration = `${this.duration}ms`;
},
cleanUpDuration(el) {
el.style.animationDuration = "";
},
setAbsolutePosition(el) {
if (this.group) {
el.style.position = "absolute";
}
}
}
};
</script>
<style>
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.fadeIn {
animation-name: fadeIn;
}
@keyframes fadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
.fadeOut {
animation-name: fadeOut;
}
.fade-move {
transition: transform 0.3s ease-out;
}
</style>
// App.vue
...
<div class="box-wrapper">
<fade-transition group :duration="300">
<div class="box"
v-for="(item, index) in list"
@click="remove(index)"
:key="item"
>
</div>
</fade-transition>
</div>
...
图片描述
完整事例地址:https://codesandbox.io/s/pk9r...
文档中介绍了一个带有transition-group元素的警告。 我们基本上必须在元素离开时将每个项目的定位设置为absolute,以实现其他项目的平滑移动动画。 我们也必须添加一个move-class并手动指定过渡持续时间,因为没有用于移动的 JS hook。我们将这些调整添加到我们的上一个示例中。
再做一些调整,通过在mixin中提取 JS 逻辑,我们可以将其应用于轻松创建新的transition组件,只需将其放入下一个项目中即可。
Vue Transition
在此之前描述的所有内容基本上都是这个小型 transition 集合所包含的内容。它有 10 个封装的transition组件,每个约1kb(缩小)。我认为它非常方便,可以轻松地在不同的项目中使用。你可以试一试:)
总结
我们从一个基本的过渡示例开始,并最终通过可调整的持续时间和transition-group支持来创建可重用的过渡组件。 我们可以使用这些技巧根据并根据自身的需求创建自己的过渡组件。 希望读者从本文中学到了一些知识,并且可以帮助你们建立功能更好的过渡组件。
今日头条作为资讯阅读类产品,Feed流场景是资讯类产品的核心场景之一,关于Feed流的改版尝试一直在进行,针对本次优化,多次在线上进行验证后,得到一个满意的结果,也将我们关于「头条首页改版」运用到的设计方法进行分享。
观点-实验-规则
项目前期提出设计论点,通过方案和实验去验证可行性,最后结合案例形成符合当前场景的设计规则,这也是本次设计探索所运用到的论证方法。
1. 什么是阅读需求?
一组信息通过不同的字号反差组合来满足不同场景下的文字阅读需求,这称之为阅读需求,阅读需求一般可以归纳为以下3种类型:快速定位型、浏览型、阅读型。
△ 三种阅读需求
2. 今日头条首页的阅读需求是什么?
今日头条Feed的阅读需求,我们定义为快速定位型和浏览型之间。原因是在阅读Feed时,用户有获取标题关键信息的强定位属性,同时也有浏览feed信息的浏览诉求;从Feed阅读习惯的分析,我们提到两个关键词,信息的定位和浏览,后面的探索也会围绕这两个关键词展开。
△ 首页的阅读需求
1. 信噪比与界面
「信噪比」(Signal-to-Noise Ratio)原本是用在声音和图像领域的概念,指在声音传播过程中信号和噪音的比例。这个理论也可以运用在页面中,如果要将讯息完整地传递给使用者,可以选择强化原有的讯息,或是降低多余的杂讯,来提高「信噪比」(Signal-to-Noise Ratio)以增加讯息成功被判读的机率,也让使用者能更轻松地阅读资讯。
一组信息是否更好的兼容定位与浏览属性,在于信息本身是否足够强调与清晰;「信噪比」理论是帮助我们理解「信息清晰度」的存在。
简而言之,「信噪比」理论通指有效信息和次级信息的比例,控制平衡这个比例,可以有利于信息完整的传递给用户,也能更轻松的获取资讯。
通过强化页面内的有效资讯,降低多余杂讯,从而提升页面内的「信噪比」,以达到提高有效资讯传递,帮助用户能更轻松获取资讯的目的。「信噪比」理论是非常通用、使用广泛的指导理论,对信息流页面设计有明确的指导作用;
△ 提升「信噪比」的目的
2. 视觉搜寻实验
△ 视觉搜寻实验
在视觉搜寻的实验里,让受试者从许多个「X」里面挑出1个「O」,然后再让他们从「┸」里面挑出一个「┼」。我们把所有的视觉元素称作刺激总量,大部分的元素(「X」和「┸」)称为干扰物,唯一不一样的那个元素(上面例子的「O」和「┼」)称为目标物。而实验的目的,就是要检测在干扰物增加和同等情况下,受试者会不会需要花费更多时间才能找到目标物。
当我们的视觉系统在辨识影像时,有一些影像的属性很容易被大脑处理,我们的视觉系统可以很快过滤辨识这些基础特征,让我们的大脑分辨这些影像属性更容易一些。因此我们涉及到大量信息的设计时,如果能够善用这些基本特征,便可以提高用户的阅读效率。
那么,什么样的特征能够帮助讯息更快,更有效的被用户判读和接收呢?在视觉搜寻实验中,有一些基础特征很容易被我们的视觉系统所辨识出来,其中主要有4种不需注意力介入便能轻易分辨的基本特征:
△ 4种基础特征
强化信息的基本特征是增强信息被判读最有效的方式,我们可以根据场景和用户诉求,选取最合理的方式来对信息进行处理;这4个基础特征,通过反差来增强核心信息的突出程度,辅助信息更好的传递给用户。
通过「视觉搜寻实验」,我们可以得到两点结论:
这两点结论是对于」信噪比「理论的补充,处理好信息的」信噪比「关系,强化有效资讯,弱化次要杂讯,帮助用户有效接收资讯,更轻松获取资讯。
3. 首页目标的变化
资讯的生命周期中,包括了产生、传递、接收这三个重要的阶段,而每个阶段都有可能造成信息的损耗;
信息产生是源头,这里的损耗在所难免,我们所要做的,是尽可能控制」传递「和」接受「阶段的损耗;早期的头条首页承载大量信息,目的是为了让用户可以接受到更多信息,这时「效率」会是第一位。
但在承载大量信息的同时,页面信息过多,容易造成信息对比弱、布局密集,从而导致信息 」传递「 阶段的损耗,同时用户在」接受「信息时,看到拥挤的信息布局,接受信息的」效率「被降低,被迫造成损耗。
为了更好的提升首页阅读效率,我们重新审视当前的设计目标,通过对不同组合的空间调整,平衡展示效率以及阅读识别性,控制「传递」和「接受」中不必要的信息损耗;将设计目标从过去的「效率最大化」,调整为「有效,轻松的阅读」
对于这个目标,我们结合之前抽离的」信噪比「理论,进行更为细致的拆解,确保能落实到后续的设计方案中。
△ 设计目标转变
4. 回顾信噪比
「信噪比」理论可以平衡页面内有效信息和次级信息,在这样理论的引导下,提供的方案其实更具备说服力。
和图片噪点一样,噪点越低,清晰度越高,映射到页面也是如此,页面内的杂讯过多,影响到有效讯息的传递,我们需要做的,就是给页面进行「降噪」处理。
1. 文字的反差比
根据前期的」信噪比「论点和」视觉搜寻实验「,我们得出两个核心观点:
两个论点其实都提到了信息对比强弱对于用户判读的影响,由此可见,不同信息的反差关系,是增强信息传递,缩短用户接受信息耗费时间的关键指标。
如果说「信噪比」是信息流优化的理论依据,那么「反差比」则是验证页面信息反差关系的手段。
调整页面内文字反差,一般通过三种方式来提升或调整
△ 提升文字反差方式
让我们来看首页中Feed流的标题字号,我们通过 iOS 和 Android 的通用文字规范可以发现,最小阅读文字为12号字(10号字用在标签,9号字用在数字提醒,都不适合作为阅读文字),Material design中正文内容默认字号为16px,iOS规范中则定义了7个正文字阶(14、15、16、17、19、21、23),综合多方因素,我们选取了16号字、17号字为主要验证目标.最后形成16/12 17/12这两组组合来验证线上Feed信息流。
「信噪比」和「视觉搜索」理论中,多次提到增强元素反差比,用户能越快获取资讯;所以我们也尝试了加粗字体和加大文字的实验尝试,用于验证文字反差比的上限。
△ iOS/Android 通用文字
2. 反差比公式
为了更好的验证和观察Feed流中的不同文字字号的反差关系范围,我们提出了一个坐标公式用于验证,可以直观的观察字号,字色,字重三组信息间的关系。
Y轴代表字号,X轴是不同灰阶文字颜色,我们可以将Feed信息组合中的字号放入表格中,通过科学方法检验反差范围。
关于这套验证公式,是我们为了验证文字反差比所提出的检验方法,通过不同实验组中字号的反差范围来验证哪组更适合Feed场景,最后输出成符合当前场景的通用规则。
△ 文字/灰阶反差比推导图
最后,我们选择4组方案进行首页反差比验证,同时将比字号放入坐标轴中,可以看出以下文字的反差范围
△ 在反差比范围内,选取的四种字体组合
上面4个表格分别对应4组反差比验证的字号组合,不同字号组合的反差比范围,我们都可视化出来,以便于更好验证哪个信噪比范围更合理;每组方案的反差比范围都不一样,我们会通过线上实验,选取最适合当前场景的反差比范围,并形成适用于Feed的反差比规则。
这里也是验证首页Feed反差比的上限和下限范围,有时主体并不是反差越强越好,反差也是有阈值范围,超过这个阈值范围,会导致信息展示比例不协调,用户侧也会起到反作用。
目前对于坐标公式验证文字反差比的理论还处于实验理论阶段,我们认为对于字号和灰阶之间,有合理的规则存在,单独对于规则的抽离和梳理,还需要大量样本去实验论证,后续有新的结论产出和模型迭代。
目前线上首页存在以下问题:
为了解决这些问题,我们对于首页的视觉语言进行了优化,通过」信噪比「理论,我们了解到平衡有效信息和杂讯的比例,是保持信息干净清晰,更好触达用户的保证;因此我们重新梳理不同题材的结构,确保核心信息在首页展示的唯一性,去掉了对于用户阅读过程中可能造成阻碍的信息。
△ 首页前后对比
这样,首页上核心信息的展示层级得到统一,保证了信息干净清晰,能够更好触达用户。
在「信噪比」理论和「反差比」实验的基础上,输出了用于线上测试的设计方案,为了验证首页中不同变量的影响,我们把头部,字号,字重,间距这些可能影响首页的因素都拆分验证对于首页影响;同时,根据前面」反差比「的验证理论,我们把」字号加大「和」文字加粗「也放进实验中进行验证,为后续实验积累数据样本。
△ 线上验证首页方案
综合前面的实验结果,最终我们选择了两组这两组方案,目前线上在进行最后实验。在实验中,我们也提取到几点关键指标:
用户对于灰头样式并未特别排斥,这个对于过去头条顶部必须是红色的认知有些挑战;其实当下设计趋势是在减少用户阅读的信息干扰,灰头更符合这一趋势,同时使用灰头也会提升头条整体计品质感,对于后续设计拓展有很大帮助。
加粗字体上,男性用户比较偏好加粗字体,但是女性用户和年轻用户较为反感。
增大Feed字号,信息展示确实更突出,但是影响到整个首页的感官,而且违背了我们的设计目标,同时也不利于后续设计拓展。部分用户手机的展示情况和特殊字体设置都会受到大字体影响,需慎重考虑。
前面有谈及18号字的问题,字号增大确实能增强视觉感官,但要考虑到头条用户比较喜欢运用特殊字体,特殊字体对比通用字体更加个性化,但是字体大小,展示高度并不可控,当字号比较大的情况下,再加上手写字体,效果不可控。
△ 线上手写字体情况
线上实验后,我们也进行了一次线下用户调研,用研结果中用户对新版满意度高于旧版,其中有一个点多次被用户提及到,就是调整后的全圆角搜索框。相比过去的方形搜索框,调整后的全圆角搜索框是能给用户留下深刻印象的视觉记忆。
回顾整个首页改版过程,我们总结了项目中的流程和思考,希望能帮助大家:
1. 定义当前使用场景的阅读需求:
阅读Feed的时候,用户有获取关键信息的强定位属性,同时也兼顾feed信息的浏览属性。
2. 通过「信噪比」理论,明确设计方向:
强化页面内的有效资讯,降低多余杂讯,提升页面内的」信噪比「,达到用户有效接受资讯,轻松获取资讯的目的。
3. 运用「反差比」手段,提升信息反差比,增强信息传递:
文字可以通过字号,字色,字重调整反差关系,并且通过「文字反差验证表」进行反差比范围验证,推导出更适合首页的反差范围,形成首页反差比规则。
4. 线上拆分验证,量化首页影响指标:
线上验证方案,把字号、字重、间距疏密,头部颜色多个维度拆分验证,观察不同个指标对首页影响。
5. 抽离项目中有价值的信息,后续形成统一规则:
间距样式和信息层级统一的feed模块;从矩形到全圆角的搜索框这些知识点都可以沉淀成信息流场景的认知规则,并且给予其它业务和项目理论和实践支持,将理论和线上验证相结合,形成真正有价值的设计方法。
「信噪比」理论可以广泛运用到页面信息设计中,帮助大家合理的梳理核心信息与次要信息关系,并且通过」反差比「手段,增强有效信息或弱化次要信息;保障页面内层级的合理布局,帮助用户更有效的判读信息;这次改版也是对目前认知基础上进行的一次拆解与构建,过去我们所认知的基础目前可能处于变化阶段,这也为我们后续方向探索提供更多可能性。
希望这些能为后续设计起到帮助,给予大家思路与灵感,更好的服务用户。
文章来源:优设 作者:今日头条UED
设计总是在变化。在过去一年里,我们不仅看见了五福的C4D运用,双11的动效插画运用,以及大量AR/VR智能的设计,还有苹果黑暗模式的普及,新技术带来新的体验和解决方案。2020年,关注研究新兴的用户体验趋势,前段时间,在我星球里,我带着设计师一起来研究关于2020界面设计趋势,希望能帮助大家是设计中有所启发。
研究背景
随着UI/UX领域人工智能的发展,和虚拟现实等新技术的变革,从前单一的内容很难满足用户的诉求,用户的诉求也变得千人前面,所以在研究趋势之前,我基于这些关键词,来分析2020的设计趋势。
色彩趋势
1.彩虹渐变
这两年来,彩色渐变一直是设计趋势,设计师将大胆的渐变,饱和度更高的渐变运用在设计中去,让整体的色彩感觉,更加年轻化与活跃。
▲ Apple一直是这个领域的引领者,随着当年iPhoneX的发布,彩虹的渐变色,大胆的渐变风格瞬间成为设计师笔下的弄潮儿。
▲ 这组插画中,作者就运用了大面积的渐变,两种颜色的运用,通过重叠,明度变化,丰富了整个设计层次。
▲ 不仅在平面设计中,在网页设计中,也是被大量运用,SWATCH的官网,清新的渐变配合动感的模特照片,以及和产品的完全结合,让页面充满着活力。
▲ 在移动端也是如此,金融产品的背景设计,银行卡面的设计,可视化图表的运用,都能看出大胆渐变还是很受欢迎。
2.黑暗模式
随着google和facebook以及instagram这些知名应用都开始提供黑暗模式,国内微信也开始黑暗模式,2020黑暗模式设计,一定是需要设计师去关注的。
黑暗模式,除了在黑暗中提高内容可读性以外,还能减少用户疲劳,还可以节约电池,所以黑暗模式在今年将是所有应用程序必不可少的一个功能。
▲ Google的黑暗模式运用,通过一个简单的按钮开关就可以切换,也可以根据系统定义切换。
▲ 黑暗模式未来会成为一种标配,一个软件设计在刚开始的时候就必须考虑进来。
▲ 关于黑暗模式,正向当年iOS扁平化刚出来的时候,国内的很多产品都还没有准备好,但是相信过不了多久,黑暗模式一定会普及开来,所以2020大家都应该提前去了解黑暗模式的,设计规范和细节。
3.更大胆的一种颜色
▲ 除了渐变色,那么大胆的单色在设计中运用也会越来越多,整个设计就一套色彩体系贯穿,大多时候以品牌色的形式去运用,配合留白,对比明显的字体,整体给人印象深刻。
▲ 在韩国的应用中用的比较多,它的优势是色彩干净,品牌整体感强,但是对设计师驾驭页面能力也要求很高,因为大面积的单色,如果用不好就会很刺眼。
▲ Naverpay的设计,整个界面就是用的绿色渐变,清新同时也能很好和Naver品牌色进行结合。
▲ 在它的很多页面中,很多控件这个绿色运用的很巧妙,不会给人很刺眼的感觉,所以大胆的一种颜色运用在2020页将是设计师值得去关注的一个方向。
▲ 扫码加入知识星球,了解更多
插画趋势
在过去几年里,插画是一种新的表达方式,越来越多的设计师,插画师通过插画表达产品的情绪,个人的情绪主张,那么在2020插画的运用就得和品牌很好的融合起来,如果你的插画是和品牌割裂的,需要注意。
1.和品牌结合
▲ wibbite的插画,标志性欧美的风格,除了插画本身手法比较简约模块,插画中运用的色彩和内容本身也很产品定位匹配。
▲ 插画对于品牌来说也是非常重要的一个因素,无论在界面设计,还是在品牌营销很多场景都需要插画去营造产品氛围和气质,插画有助于将我们的品牌故事讲述给听众听,所以在构建一个品牌时候,插画是非常重要的点之一,但是做之前一定要尽量多去了解我们为客户提供的设计价值,只有了解了用户价值,才能去定义去特色的故事,帮助产品。
▲ Uber系列插画提供暖色,以及安全蓝的运用,突出打车服务中安全的关键要素。
▲ Google的插画系统也是如此,没有很华丽的炫技,有的是对于多元文化的思考,设计场景的融合贯通。
所以不难看出,插画的方向一定是和品牌结合的,不会为了趋势而趋势,一定是围绕内容去设计。
2.3D插画
▲ 如果说这2年,各种各样的插画手法百花齐放,那么随着人们的审美变化,趋势也从静态变成动态,从平面变成3D,从今年的支付宝五福设计中大家应该能感觉到2020的插画设计一定是往着3D方向,而且是动态3D方向演进的。
▲ 谷歌一组插画,结合与大脑、团队合作、想法、密码箱为图形进行创意设计,人物造型可爱好玩,凸显年轻化潮流。
▲ Apple在中国区App Store 的一组设计,整体设计以红色为主,运用了象征中国元素的龙、红包、灯笼、福字、纸牌作为设计元素,整体运用3D表达,凸显年轻和趣味性。
3.等距插画
等距插画这两年已经是一个主流风格之一,在未来还会接着流行,但是等距插画未来或许和场景联系在更紧密,在每年天猫双11设计中,互动城的设计每年基本都是等距插画风格,它的故事感和画面感,都能让人眼前一亮,所以电商的设计,在短时间内,大型活动场景基本都离不开这个风格。
▲ 这组等距插画场景故事感很强,建立了一个空间世界,作为网站主风格非常的吸引人。
▲ 除了这种大的场景,等距插画,在小的场景中,作为插画规范也是运用比较多,它风格可轻可重,随着5G时代到来,静态可能会逐步演进成动态插画。
4.根据内容定制的插画
随着内容的升级,对内容的表达也会被越来越重视,那么如何更好的把内容更生动表达出来,插画就是很好的一个形式,根据内容定制的插画,在很多产品中被运用到,互联网教育,IT等领域。所以专门针对内容去设计插画,在今年或许是一个趋势,插画不再是孤零零只是为了情感化而存在。
▲ Crowdrise的产品设计,整体就是运用作为整体设计语言,风格统一,内容突出,画面效果好。
▲ 插画的形态终于不再是孤零零的只是用户情感的表达,而是随着内容的升级变化,在产品中发挥的重要性也越来越大。
3.3D黏土插画的运用
3D设计因为今年设计师很喜欢用的样式之一,我们将尽管3D已经存在了一段时间,但最近我们看到了很多模仿粘土样式效果的3D插图的兴起。这将成为今年流行的设计趋势,我们甚至还会看到很多艺术家在电影,插画和广告中结合了2D和3D风格。
▲ 在instagran的一组设计中,设计师运用这种黏土和3D的方式设计,画面充满了生活感和温暖。
▲ 谷歌也在它们的项目中大量运用这种黏土风格设计,会显得更加的现代和活泼,黏土的设计相对3D插画区别在于更加细腻,线条上更加柔和。
字体趋势
这几年越来越多的公司,开始定制字体,根据自己的品牌特质去做一些独特的字体,从国外到国内,品牌定制字体未来或许还会越来越多,对于设计师好处来说,这些品牌字体很多都是免费开放给设计师用,会形成一个很高的商业环境。
1.定制字体
▲ 锤子发布了他们的Smartisan T黑字体。
▲ 腾讯发布了定制的“腾讯字体”,相比原来倾斜的黑体字,腾讯的新字体更现代、更协调、更动感。运用起来也非常有力量感。
▲ 阿里普惠体,随着集团业务庞大,字体使用场景多且复杂,包括各产品客户端、营销设计、操作系统、硬件设备、建筑空间及公关传播等等。决定以现代为核心设计理念,以可靠、明快为设计切入点,将阿里精神融入到字体当中。最终一款拥有现代感满足全场景黑体诞生了,而且商用免费。
▲ 除了前面的阿里,腾讯,锤子,小米OPPO,京东也都发布了自己的专属品牌字体,所以在2020年,品牌字体或许是每个公司的一个标配了,我们拭目以待。
▲ 国外其实是最早开始为品牌定制字体的,三星手机虽然销量消化,但是他们品牌字体:SamsungOne,设计风格,很强的现代感,而且,这个字体有不同粗细的笔画字重,适用面很广。
▲ IBM的字体名称:IBM Plex Sans Text,这款字体设计比较简洁,干净,没有多余的笔画,这个字体很良心免费商用的。
2.粗体的运用
纵观2020的一些设计,留白越来越大,边距越留越多,字体也变的很粗,粗的字体和正文普通字体形成了明显的对比,再加上网格的布局,让页面内容更加凸显。
▲ 大字体的运用,字体就是内容,重复运用内容元素作为排版手段,对于内容组织和平面要求较高。
▲ 在UI设计中,大字体的设计也很常见,苹果商店,苹果官方应用都是大字体的推崇者,随便5G的来临,对于内容的追求也会越来越高,那么除了大字体,视觉元素减少,内容本身质量要求也越来越高。
▲ 粗的字体常用语大标题,或者页面导航性指引作业,帮助用户更好去理解功能本身,上面这个页面粗的字体就是导航,告诉用户这一页,你需要去完成什么动作指引。
▲ 扫码加入知识星球,了解更多
最后
设计趋势必然与技术发展紧密结合,作为设计师我们需要了解,以及平时和我们设计进行结合,下期将带来,2020的UI设计趋势下部分,看看还有哪些需要我们去关注的。
转自:ui中国-skys
XSS
跨站脚本攻击(Cross Site Script),本来缩写是 CSS, 但是为了和层叠样式表(Cascading Style Sheet, CSS)有所区分,所以安全领域叫做 “XSS”;
XSS攻击,通常是指攻击者通过 “HTML注入”篡改了网页,插入了恶意的脚本,从而在用户浏览网页时,对用户的浏览器进行控制或者获取用户的敏感信息(Cookie, SessionID等)的一种攻击方式。
页面被注入了恶意JavaScript脚本,浏览器无法判断区分这些脚本是被恶意注入的,还是正常的页面内容,所以恶意注入Javascript脚本也拥有了所有的脚本权限。如果页面被注入了恶意 JavaScript脚本,它可以做哪些事情呢?
可以窃取 cookie信息。恶意 JavaScript可以通过 ”doccument.cookie“获取cookie信息,然后通过 XMLHttpRequest或者Fetch加上CORS功能将数据发送给恶意服务器;恶意服务器拿到用户的cookie信息之后,就可以在其他电脑上模拟用户的登陆,然后进行转账操作。
可以监听用户行为。恶意JavaScript可以使用 "addEventListener"接口来监听键盘事件,比如可以获取用户输入的银行卡等信息,又可以做很多违法的事情。
可以修改DOM 伪造假的登陆窗口,用来欺骗用户输入用户名和密码等信息。
还可以在页面内生成浮窗广告,这些广告会严重影响用户体验。
XSS攻击可以分为三类:反射型,存储型,基于DOM型(DOM based XSS)
反射型
恶意脚本作为网络请求的一部分。
const Koa = require("koa");
const app = new Koa();
app.use(async ctx => {
// ctx.body 即服务端响应的数据
ctx.body = '<script>alert("反射型 XSS 攻击")</script>';
})
app.listen(3000, () => {
console.log('启动成功');
});
访问 http://127.0.0.1:3000/ 可以看到 alert执行
反射型XSS1
举一个常见的场景,我们通过页面的url的一个参数来控制页面的展示内容,比如我们把上面的一部分代码改成下面这样
app.use(async ctx => {
// ctx.body 即服务端响应的数据
ctx.body = ctx.query.userName;
})
此时访问 http://127.0.0.1:3000?userName=xiaoming 可以看到页面上展示了xiaoming,此时我们访问 http://127.0.0.1:3000?userName=<script>alert("反射型 XSS 攻击")</script>, 可以看到页面弹出 alert。
反射型XSS2
通过这个操作,我们会发现用户将一段含有恶意代码的请求提交给服务器,服务器在接收到请求时,又将恶意代码反射给浏览器端,这就是反射型XSS攻击。另外一点需要注意的是,Web 服务器不会存储反射型 XSS 攻击的恶意脚本,这是和存储型 XSS 攻击不同的地方。
在实际的开发过程中,我们会碰到这样的场景,在页面A中点击某个操作,这个按钮操作是需要登录权限的,所以需要跳转到登录页面,登录完成之后再跳转会A页面,我们是这么处理的,跳转登录页面的时候,会加一个参数 returnUrl,表示登录完成之后需要跳转到哪个页面,即这个地址是这样的 http://xxx.com/login?returnUrl=http://xxx.com/A,假如这个时候把returnUrl改成一个script脚本,而你在登录完成之后,如果没有对returnUrl进行合法性判断,而直接通过window.location.href=returnUrl,这个时候这个恶意脚本就会执行。
存储型
存储型会把用户输入的数据“存储”在服务器。
比较常见的一个场景就是,攻击者在社区或论坛写下一篇包含恶意 JavaScript代码的博客文章或评论,文章或评论发表后,所有访问该博客文章或评论的用户,都会在他们的浏览器中执行这段恶意的JavaScript代码。
存储型攻击大致需要经历以下几个步骤
首先攻击者利用站点漏洞将一段恶意JavaScript代码提交到网站数据库中
然后用户向网站请求包含了恶意 JavaScript脚本的页面
当用户浏览该页面的时候,恶意脚本就会将用户的cookie信息等数据上传到服务器
存储型XSS
举一个简单的例子,一个登陆页面,点击登陆的时候,把数据存储在后端,登陆完成之后跳转到首页,首页请求一个接口将当前的用户名显示到页面
客户端代码
<!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>XSS-demo</title>
<style>
.login-wrap {
height: 180px;
width: 300px;
border: 1px solid #ccc;
padding: 20px;
margin-bottom: 20px;
}
input {
width: 300px;
}
</style>
</head>
<body>
<div class="login-wrap">
<input type="text" placeholder="用户名" class="userName">
<br>
<input type="password" placeholder="密码" class="password">
<br>
<br>
<button class="btn">登陆</button>
</div>
</body>
<script>
var btn = document.querySelector('.btn');
btn.onclick = function () {
var userName = document.querySelector('.userName').value;
var password = document.querySelector('.password').value;
fetch('http://localhost:3200/login', {
method: 'POST',
body: JSON.stringify({
userName,
password
}),
headers:{
'Content-Type': 'application/json'
},
mode: 'cors'
})
.then(function (response) {
return response.json();
})
.then(function (res) {
alert(res.msg);
window.location.href= "http://localhost:3200/home";
})
.catch(err => {
message.error(`本地测试错误 ${err.message}`);
console.error('本地测试错误', err);
});
}
</script>
</html>
服务端代码
const Koa = require("koa");
const app = new Koa();
const route = require('koa-route');
var bodyParser = require('koa-bodyparser');
const cors = require('@koa/cors');
// 临时用一个变量来存储,实际应该存在数据库中
let currentUserName = '';
app.use(bodyParser()); // 处理post请求的参数
const login = ctx => {
const req = ctx.request.body;
const userName = req.userName;
currentUserName = userName;
ctx.response.body = {
msg: '登陆成功'
};
}
const home = ctx => {
ctx.body = currentUserName;
}
app.use(cors());
app.use(route.post('/login', login));
app.use(route.get('/home', home));
app.listen(3200, () => {
console.log('启动成功');
});
点击登陆将输入信息提交大服务端,服务端使用变量 currentUserName来存储当前的输入内容,登陆成功后,跳转到 首页, 服务端会返回当前的用户名。如果用户输入了恶意脚本内容,则恶意脚本就会在浏览器端执行。
在用户名的输入框输入 <script>alert('存储型 XSS 攻击')</script>,执行结果如下
存储型XSS
基于DOM(DOM based XSS)
通过恶意脚本修改页面的DOM节点,是发生在前端的攻击
基于DOM攻击大致需要经历以下几个步骤
攻击者构造出特殊的URL,其中包含恶意代码
用户打开带有恶意代码的URL
用户浏览器接受到响应后执行解析,前端JavaScript取出URL中的恶意代码并执行
恶意代码窃取用户数据并发送到攻击者的网站,冒充用户行为,调用目标网站接口执行攻击者指定的操作。
举个例子:
<body>
<div class="login-wrap">
<input type="text" placeholder="输入url" class="url">
<br>
<br>
<button class="btn">提交</button>
<div class="content"></div>
</div>
</body>
<script>
var btn = document.querySelector('.btn');
var content = document.querySelector('.content');
btn.onclick = function () {
var url = document.querySelector('.url').value;
content.innerHTML = `<a href=${url}>跳转到输入的url</a>`
}
</script>
点击提交按钮,会在当前页面插入一个超链接,其地址为文本框的内容。
在输入框输入 如下内容
'' onclick=alert('哈哈,你被攻击了')
执行结果如下
基于DOM型XSS
首先用两个单引号闭合调 href属性,然后插入一个onclick事件。点击这个新生成的链接,脚本将被执行。
上面的代码是通过执行 执行 alert来演示的攻击类型,同样你可以把上面的脚本代码修改为任何你想执行的代码,比如获取 用户的 cookie等信息,<script>alert(document.cookie)</script>,同样也是可以的.
防御XSS
HttpOnly
由于很多XSS攻击都是来盗用Cookie的,因此可以通过 使用HttpOnly属性来防止直接通过 document.cookie 来获取 cookie。
一个Cookie的使用过程如下
浏览器向服务器发起请求,这时候没有 Cookie
服务器返回时设置 Set-Cookie 头,向客户端浏览器写入Cookie
在该 Cookie 到期前,浏览器访问该域下的所有页面,都将发送该Cookie
HttpOnly是在 Set-Cookie时标记的:
通常服务器可以将某些 Cookie 设置为 HttpOnly 标志,HttpOnly 是服务器通过 HTTP 响应头来设置的。
const login = ctx => {
// 简单设置一个cookie
ctx.cookies.set(
'cid',
'hello world',
{
domain: 'localhost', // 写cookie所在的域名
path: '/home', // 写cookie所在的路径
maxAge: 10 * 60 * 1000, // cookie有效时长
expires: new Date('2021-02-15'), // cookie失效时间
httpOnly: true, // 是否只用于http请求中获取
overwrite: false // 是否允许重写
}
)
}
HttpOnly
需要注意的一点是:HttpOnly 并非阻止 XSS 攻击,而是能阻止 XSS 攻击后的 Cookie 劫持攻击。
输入和输出的检查
永远不要相信用户的输入。
输入检查一般是检查用户输入的数据是都包含一些特殊字符,如 <、>, '及"等。如果发现特殊字符,则将这些字符过滤或编码。这种可以称为 “XSS Filter”。
安全的编码函数
针对HTML代码的编码方式是 HtmlEncode(是一种函数实现,将字符串转成 HTMLEntrities)
& --> &
< --> <
> --> >
" --> "
相应的, JavaScript的编码方式可以使用 JavascriptEncode。
假如说用户输入了 <script>alert("你被攻击了")</script>,我们要对用户输入的内容进行过滤(如果包含了 <script> 等敏感字符,就过滤掉)或者对其编码,如果是恶意的脚本,则会变成下面这样
<script>alert("你被攻击了");</script>
经过转码之后的内容,如 <script>标签被转换为 <script>,即使这段脚本返回给页面,页面也不会指向这段代码。
防御 DOM Based XSS
我们可以回看一下上面的例子
btn.onclick = function () {
var url = document.querySelector('.url').value;
content.innerHTML = `<a href=${url}>跳转到输入的url</a>`
}
事实上,DOM Based XSS 是从 JavaScript中输出数据到HTML页面里。
用户输入 '' onclick=alert('哈哈,你被攻击了'),然后通过 innerHTML 修改DOM的内容,就变成了 <a href='' onclick=alert('哈哈,你被攻击了')>跳转到输入的url</a>, XSS因此产生。
那么正确的防御方法是什么呢?
从JavaScript输出到HTML页面,相当于一次 XSS输出的过程,需要根据不同场景进行不同的编码处理
变量输出到 <script>,执行一次 JavascriptEncode
通过JS输出到HTML页面
输出事件或者脚本,做 JavascriptEncode 处理
输出 HTML内容或者属性,做 HtmlEncode 处理
会触发 DOM Based XSS的地方有很多,比如
xxx.interHTML
xxx.outerHTML
document.write
页面中所有的inputs框
XMLHttpRequest返回的数据
...
项目中如果用到,一定要避免在字符串中拼接不可信的数据。
利用CSP
CSP (Content Security Policy) 即内容安全策略,是一种可信白名单机制,可以在服务端配置浏览器哪些外部资源可以加载和执行。我们只需要配置规则,如何拦截是由浏览器自己实现的。我们可以通过这种方式来尽量减少 XSS 攻击。
通常可以通过两种方式来开启 CSP:
设置 HTTP Header 的 Content-Security-Policy
Content-Security-Policy: default-src 'self'; // 只允许加载本站资源
Content-Security-Policy: img-src https://* // 只允许加载 HTTPS 协议图片
Content-Security-Policy: child-src 'none' // 允许加载任何来源框架
设置 meta 标签的方式
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src https://*; child-src 'none';">
蓝蓝设计的小编 http://www.lanlanwork.com