上一篇博客讲到了数组的方法,当然里边比较复杂的就是数组的迭代方法,因为涉及到了回调函数,所以这篇博客我们来详细讲解一下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
原始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支持来创建可重用的过渡组件。 我们可以使用这些技巧根据并根据自身的需求创建自己的过渡组件。 希望读者从本文中学到了一些知识,并且可以帮助你们建立功能更好的过渡组件。
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';">
css 伪类是用于向某些选择器添加特殊的效果,是动态的,指当前元素所处的状态或者特性。只有一个元素达到一个特定状态时,它可能得到一个伪类的样式;当状态改变时,它又会失去这个样式。
这篇文章在一定程度上鼓励你在构建UI时使用更简单的CSS和更少的 JS。熟悉 CSS 所提供的一切是实现这一目标的一种方法,另一种方法是实现最佳实践并尽可能多地重用代码。
接下介绍一些大家可能还不熟悉的一些伪类及其用例,希望对大家日后有所帮助。
::first-line | 选择文本的第一行
::first-line 伪元素在某块级元素的第一行应用样式。第一行的长度取决于很多因素,包括元素宽度,文档宽度和文本的文字大小。
::first-line 伪元素只能在块容器中,所以,::first-line伪元素只能在一个display值为block, inline-block, table-cell 或者 table-caption中有用。在其他的类型中,::first-line 是不起作用的。
用法如下:
p:first-line {
color: lightcoral;
}
::first-letter | 选择这一行的第一字
CSS 伪元素 ::first-letter会选中某块级元素第一行的第一个字母。用法如下:
<style>
p::first-letter{
color: red;
font-size: 2em;
}
</style>
<p>前端小智,不断努,终身学习者!</p>
clipboard.png
::selection| 被用户高亮的部分
::selection 伪元素应用于文档中被用户高亮的部分(比如使用鼠标或其他选择设备选中的部分)。
div::selection {
color: #409EFF;
}
clipboard.png
:root | 根元素
:root 伪类匹配文档树的根元素。对于 HTML 来说,:root 表示 <html> 元素,除了优先级更高之外,与 html 选择器相同。
在声明全局 CSS 变量时 :root 会很有用:
:root {
--main-color: hotpink;
--pane-padding: 5px 42px;
}
:empty | 仅当子项为空时才有作用
:empty 伪类代表没有子元素的元素。子元素只可以是元素节点或文本(包括空格),注释或处理指令都不会产生影响。
div:empty {
border: 2px solid orange;
margin-bottom: 10px;
}
<div></div>
<div></div>
<div>
</div>
clipboard.png
只有第一个和第二个div有作用,因为它们确实是空的,第三个 div 没有作用,因为它有一个换行。
:only-child | 只有一个子元素才有作用
:only-child 匹配没有任何兄弟元素的元素.等效的选择器还可以写成 :first-child:last-child或者:nth-child(1):nth-last-child(1),当然,前者的权重会低一点。
p:only-child{
background: #409EFF;
}
<div>
<p>第一个没有任何兄弟元素的元素</p>
</div>
<div>
<p>第二个</p>
<p>第二个</p>
</div>
clipboard.png
:first-of-type | 选择指定类型的第一个子元素
:first-of-type表示一组兄弟元素中其类型的第一个元素。
.innerDiv p:first-of-type {
color: orangered;
}
上面表示将 .innerDiv 内的第一个元素为 p 的颜色设置为橘色。
<div class="innerDiv">
<div>Div1</div>
<p>These are the necessary steps</p>
<p>hiya</p>
<p>
Do <em>not</em> push the brake at the same time as the accelerator.
</p>
<div>Div2</div>
</div>
clipboard.png
:last-of-type | 选择指定类型的最后一个子元素
:last-of-type CSS 伪类 表示了在(它父元素的)子元素列表中,最后一个给定类型的元素。当代码类似Parent tagName:last-of-type的作用区域包含父元素的所有子元素中的最后一个选定元素,也包括子元素的最后一个子元素并以此类推。
.innerDiv p:last-of-type {
color: orangered;
}
上面表示将 .innerDiv 内的的最后一个元素为 p 的颜色设置为橘色。
clipboard.png
nth-of-type() | 选择指定类型的子元素
:nth-of-type() 这个 CSS 伪类是针对具有一组兄弟节点的标签, 用 n 来筛选出在一组兄弟节点的位置。
.innerDiv p:nth-of-type(1) {
color: orangered;
}
<div class="innerDiv">
<div>Div1</div>
<p>These are the necessary steps</p>
<p>hiya</p>
<p>
Do <em>not</em> push the brake at the same time as the accelerator.
</p>
<div>Div2</div>
</div>
clipboard.png
:nth-last-of-type() | 在列表末尾选择类型的子元素
:nth-last-of-type(an+b) 这个 CSS 伪类 匹配那些在它之后有 an+b-1 个相同类型兄弟节点的元素,其中 n 为正值或零值。它基本上和 :nth-of-type 一样,只是它从结尾处反序计数,而不是从开头处。
.innerDiv p:nth-last-of-type(1) {
color: orangered;
}
这会选择innerDiv元素中包含的类型为p元素的列表中的最后一个子元素。
<div class="innerDiv">
<p>These are the necessary steps</p>
<p>hiya</p>
<div>Div1</div>
<p>
Do the same.
</p>
<div>Div2</div>
</div>
clipboard.png
:link | 选择一个未访问的超链接
:link伪类选择器是用来选中元素当中的链接。它将会选中所有尚未访问的链接,包括那些已经给定了其他伪类选择器的链接(例如:hover选择器,:active选择器,:visited选择器)。
为了可以正确地渲染链接元素的样式,:link伪类选择器应当放在其他伪类选择器的前面,并且遵循LVHA的先后顺序,即::link — :visited — :hover — :active。:focus伪类选择器常伴随在:hover伪类选择器左右,需要根据你想要实现的效果确定它们的顺序。
a:link {
color: orangered;
}
<a href="/login">Login<a>
clipboard.png
:checked | 选择一个选中的复选框
:checked CSS 伪类选择器表示任何处于选中状态的radio(<input type="radio">), checkbox (<input type="checkbox">) 或("select") 元素中的option HTML元素("option")。
input:checked {
box-shadow: 0 0 0 3px hotpink;
}
<input type="checkbox" />
clipboard.png
大家都说简历没项目写,我就帮大家找了一个项目,还附赠【搭建教程】。
:valid | 选择一个有效的元素
:valid CSS 伪类表示内容验证正确的<input> 或其他 <form> 元素。这能简单地将校验字段展示为一种能让用户辨别出其输入数据的正确性的样式。
input:valid {
box-shadow: 0 0 0 3px hotpink;
}
clipboard.png
:invalid | 选择一个无效的元素
:invalid CSS 伪类 表示任意内容未通过验证的 <input> 或其他 <form> 元素。
input[type="text"]:invalid {
border-color: red;
}
:lang() | 通过指定的lang值选择一个元素
:lang() CSS 伪类基于元素语言来匹配页面元素。
/* 选取任意的英文(en)段落 */
p:lang(en) {
quotes: '\201C' '\201D' '\2018' '\2019';
}
:not() | 用来匹配不符合一组选择器的元素
CSS 伪类 :not() 用来匹配不符合一组选择器的元素。由于它的作用是防止特定的元素被选中,它也被称为反选伪类(negation pseudo-class)。
来看一个例子:
.innerDiv :not(p) {
color: lightcoral;
}
<div class="innerDiv">
<p>Paragraph 1</p>
<p>Paragraph 2</p>
<div>Div 1</div>
<p>Paragraph 3</p>
<div>Div 2</div>
</div>
clipboard.png
Div 1 和 Div 2会被选中,p 不会被选 中。
原文:https://blog.bitsrc.io/css-ps...
代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug。
什么是图片懒加载
当我们向下滚动的时候图片资源才被请求到,这也就是我们本次要实现的效果,进入页面的时候,只请求可视区域的图片资源这也就是懒加载。
比如我们加载一个页面,这个页面很长很长,长到我们的浏览器可视区域装不下,那么懒加载就是优先加载可视区域的内容,其他部分等进入了可视区域在加载。
这个功能非常常见,你打开淘宝的首页,向下滚动,就会看到会有图片不断的加载;你在百度中搜索图片,结果肯定成千上万条,不可能所有的都一下子加载出来的,很重要的原因就是会有性能问题。你可以在Network中查看,在页面滚动的时候,会看到图片一张张加载出来。
lazyLoad
为什么要做图片懒加载
懒加载是一种网页性能优化的方式,它能极大的提升用户体验。就比如说图片,图片一直是影响网页性能的主要元凶,现在一张图片超过几兆已经是很经常的事了。如果每次进入页面就请求所有的图片资源,那么可能等图片加载出来用户也早就走了。所以,我们需要懒加载,进入页面的时候,只请求可视区域的图片资源。
总结出来就两个点:
1.全部加载的话会影响用户体验
2.浪费用户的流量,有些用户并不像全部看完,全部加载会耗费大量流量。
懒加载原理
图片的标签是 img标签,图片的来源主要是 src属性,浏览器是否发起加载图片的请求是根据是否有src属性决定的。
所以可以从 img标签的 src属性入手,在没进到可视区域的时候,就先不给 img 标签的 src属性赋值。
懒加载实现
实现效果图:
imgLazyLoad
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
div {
display: flex;
flex-direction: column;
}
img {
width: 100%;
height: 300px;
}
</style>
</head>
<body>
<div>
<img data-src="https://cdn.suisuijiang.com/ImageMessage/5adad39555703565e79040fa_1590657907683.jpeg">
<img data-src="https://cdn.suisuijiang.com/ImageMessage/5adad39555703565e79040fa_1590657913523.jpeg">
<img data-src="https://cdn.suisuijiang.com/ImageMessage/5adad39555703565e79040fa_1590657925550.jpeg">
<img data-src="https://cdn.suisuijiang.com/ImageMessage/5adad39555703565e79040fa_1590657930289.jpeg">
<img data-src="https://cdn.suisuijiang.com/ImageMessage/5adad39555703565e79040fa_1590657934750.jpeg">
<img data-src="https://cdn.suisuijiang.com/ImageMessage/5adad39555703565e79040fa_1590657918315.jpeg">
</div>
</body>
</html>
监听 scroll 事件判断元素是否进入视口
const imgs = [...document.getElementsByTagName('img')];
let n = 0;
lazyload();
function throttle(fn, wait) {
let timer = null;
return function(...args) {
if(!timer) {
timer = setTimeout(() => {
timer = null;
fn.apply(this, args)
}, wait)
}
}
}
function lazyload() {
var innerHeight = window.innerHeight;
var scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
for(let i = n; i < imgs.length; i++) {
if(imgs[i].offsetTop < innerHeight + scrollTop) {
imgs[i].src = imgs[i].getAttribute("data-src");
n = i + 1;
}
}
}
window.addEventListener('scroll', throttle(lazyload, 200));
可能会存在下面几个问题:
每次滑动都要执行一次循环,如果有1000多个图片,性能会很差
每次读取 scrollTop 都会引起回流
scrollTop跟DOM的嵌套关系有关,应该根据getboundingclientrect获取
滑到最后的时候刷新,会看到所有的图片都加载了
IntersectionObserver
Intersection Observer API提供了一种异步观察目标元素与祖先元素或文档viewport的交集中的变化的方法。
创建一个 IntersectionObserver对象,并传入相应参数和回调用函数,该回调函数将会在目标(target)元素和根(root)元素的交集大小超过阈值(threshold)规定的大小时候被执行。
var observer = new IntersectionObserver(callback, options);
IntersectionObserver是浏览器原生提供的构造函数,接受两个参数:callback是可见性变化时的回调函数(即目标元素出现在root选项指定的元素中可见时,回调函数将会被执行),option是配置对象(该参数可选)。
返回的 observer是一个观察器实例。实例的 observe 方法可以指定观察哪个DOM节点。
具体的用法可以 查看 MDN文档
const imgs = [...document.getElementsByTagName('img')];
// 当监听的元素进入可视范围内的会触发回调
if(IntersectionObserver) {
// 创建一个 intersection observer
let lazyImageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach((entry, index) => {
let lazyImage = entry.target;
// 相交率,默认是相对于浏览器视窗
if(entry.intersectionRatio > 0) {
lazyImage.src = lazyImage.getAttribute('data-src');
// 当前图片加载完之后需要去掉监听
lazyImageObserver.unobserve(lazyImage);
}
})
})
for(let i = 0; i < imgs.length; i++) {
lazyImageObserver.observe(imgs[i]);
}
}
源码地址-codePen点击预览
vue自定义指令-懒加载
Vue自定义指令
下面的api来自官网自定义指令:
钩子函数
bind: 只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
inserted: 被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
update: 所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新
componentUpdated: 指令所在组件的 VNode 及其子 VNode 全部更新后调用。
unbind: 只调用一次,指令与元素解绑时调用。
钩子函数参数
指令钩子函数会被传入以下参数:
el:指令所绑定的元素,可以用来直接操作 DOM。
binding:一个对象,包含以下 property:
name:指令名,不包括 v- 前缀。
value:指令的绑定值,例如:v-my-directive="1 + 1" 中,绑定值为 2。
oldValue:指令绑定的前一个值,仅在 update 和 componentUpdated 钩子中可用。无论值是否改变都可用。
expression:字符串形式的指令表达式。例如 v-my-directive="1 + 1" 中,表达式为 "1 + 1"。
arg:传给指令的参数,可选。例如 v-my-directive:foo 中,参数为 "foo"。
modifiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true }。
vnode:Vue 编译生成的虚拟节点。
oldVnode:上一个虚拟节点,仅在 update 和 componentUpdated 钩子中可用。
实现 v-lazyload 指令
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
img {
width: 100%;
height: 300px;
}
</style>
</head>
<body>
<div id="app">
<p v-for="item in imgs" :key="item">
<img v-lazyload="item">
</p>
</div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<script>
Vue.directive("lazyload", {
// 指令的定义
bind: function(el, binding) {
火车车次
/^[GCDZTSPKXLY1-9]\d{1,4}$/
手机机身码(IMEI)
/^\d{15,17}$/
必须带端口号的网址(或ip)
/^((ht|f)tps?:\/\/)?[\w-]+(\.[\w-]+)+:\d{1,5}\/?$/
网址(url,支持端口和"?+参数"和"#+参数)
/^(((ht|f)tps?):\/\/)?[\w-]+(\.[\w-]+)+([\w.,@?^=%&:\/~+#-]*[\w@?^=%&\/~+#-])?$/
统一社会信用代码
/^[0-9A-HJ-NPQRTUWXY]{2}\d{6}[0-9A-HJ-NPQRTUWXY]{10}$/
迅雷链接
/^thunderx?:\/\/[a-zA-Z\d]+=$/
ed2k链接(宽松匹配)
/^ed2k:\/\/\|file\|.+\|\/$/
磁力链接(宽松匹配)
/^magnet:\?xt=urn:btih:[0-9a-fA-F]{40,}.*$/
子网掩码
/^(?:\d{1,2}|1\d\d|2[0-4]\d|25[0-5])(?:\.(?:\d{1,2}|1\d\d|2[0-4]\d|25[0-5])){3}$/
linux"隐藏文件"路径
/^\/(?:[^\/]+\/)*\.[^\/]*/
linux文件夹路径
/^\/(?:[^\/]+\/)*$/
linux文件路径
/^\/(?:[^\/]+\/)*[^\/]+$/
window"文件夹"路径
/^[a-zA-Z]:\\(?:\w+\\?)*$/
window下"文件"路径
/^[a-zA-Z]:\\(?:\w+\\)*\w+\.\w+$/
股票代码(A股)
/^(s[hz]|S[HZ])(000[\d]{3}|002[\d]{3}|300[\d]{3}|600[\d]{3}|60[\d]{4})$/
大于等于0, 小于等于150, 支持小数位出现5, 如145.5, 用于判断考卷分数
/^150$|^(?:\d|[1-9]\d|1[0-4]\d)(?:.5)?$/
html注释
/^<!--[\s\S]*?-->$/
md5格式(32位)
/^([a-f\d]{32}|[A-F\d]{32})$/
版本号(version)格式必须为X.Y.Z
/^\d+(?:\.\d+){2}$/
视频(video)链接地址(视频格式可按需增删)
/^https?:\/\/(.+\/)+.+(\.(swf|avi|flv|mpg|rm|mov|wav|asf|3gp|mkv|rmvb|mp4))$/i
图片(image)链接地址(图片格式可按需增删)
/^https?:\/\/(.+\/)+.+(\.(gif|png|jpg|jpeg|webp|svg|psd|bmp|tif))$/i
24小时制时间(HH:mm:ss)
/^(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d$/
12小时制时间(hh:mm:ss)
/^(?:1[0-2]|0?[1-9]):[0-5]\d:[0-5]\d$/
base64格式
/^\s*data:(?:[a-z]+\/[a-z0-9-+.]+(?:;[a-z-]+=[a-z0-9-]+)?)?(?:;base64)?,([a-z0-9!$&',()*+;=\-._~:@\/?%\s]*?)\s*$/i
数字/货币金额(支持负数、千分位分隔符)
/^-?\d+(,\d{3})*(\.\d{1,2})?$/
数字/货币金额 (只支持正数、不支持校验千分位分隔符)
/(?:^[1-9]([0-9]+)?(?:\.[0-9]{1,2})?$)|(?:^(?:0){1}$)|(?:^[0-9]\.[0-9](?:[0-9])?$)/
银行卡号(10到30位, 覆盖对公/私账户, 参考微信支付)
/^[1-9]\d{9,29}$/
中文姓名
/^(?:[\u4e00-\u9fa5·]{2,16})$/
英文姓名
/(^[a-zA-Z]{1}[a-zA-Z\s]{0,20}[a-zA-Z]{1}$)/
车牌号(新能源)
/[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领 A-Z]{1}[A-HJ-NP-Z]{1}(([0-9]{5}[DF])|([DF][A-HJ-NP-Z0-9][0-9]{4}))$/
车牌号(非新能源)
/^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领 A-Z]{1}[A-HJ-NP-Z]{1}[A-Z0-9]{4}[A-Z0-9挂学警港澳]{1}$/
车牌号(新能源+非新能源)
/^(?:[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领 A-Z]{1}[A-HJ-NP-Z]{1}(?:(?:[0-9]{5}[DF])|(?:[DF](?:[A-HJ-NP-Z0-9])[0-9]{4})))|(?:[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领 A-Z]{1}[A-Z]{1}[A-HJ-NP-Z0-9]{4}[A-HJ-NP-Z0-9 挂学警港澳]{1})$/
手机号(mobile phone)中国(严谨), 根据工信部2019年公布的手机号段
/^(?:(?:\+|00)86)?1(?:(?:3[\d])|(?:4[5-7|9])|(?:5[0-3|5-9])|(?:6[5-7])|(?:7[0-8])|(?:8[\d])|(?:9[1|8|9]))\d{8}$/
手机号(mobile phone)中国(宽松), 只要是13,14,15,16,17,18,19开头即可
/^(?:(?:\+|00)86)?1[3-9]\d{9}$/
手机号(mobile phone)中国(最宽松), 只要是1开头即可, 如果你的手机号是用来接收短信, 优先建议选择这一条
/^(?:(?:\+|00)86)?1\d{10}$/
date(日期)
/^\d{4}(-)(1[0-2]|0?\d)\1([0-2]\d|\d|30|31)$/
email(邮箱)
/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
座机(tel phone)电话(国内),如: 0341-86091234
/^\d{3}-\d{8}$|^\d{4}-\d{7}$/
身份证号(1代,15位数字)
/^[1-9]\d{7}(?:0\d|10|11|12)(?:0[1-9]|[1-2][\d]|30|31)\d{3}$/
身份证号(2代,18位数字),最后一位是校验位,可能为数字或字符X
/^[1-9]\d{5}(?:18|19|20)\d{2}(?:0\d|10|11|12)(?:0[1-9]|[1-2]\d|30|31)\d{3}[\dXx]$/
身份证号, 支持1/2代(15位/18位数字)
/(^\d{8}(0\d|10|11|12)([0-2]\d|30|31)\d{3}$)|(^\d{6}(18|19|20)\d{2}(0\d|10|11|12)([0-2]\d|30|31)\d{3}(\d|X|x)$)/
护照(包含香港、澳门)
/(^[EeKkGgDdSsPpHh]\d{8}$)|(^(([Ee][a-fA-F])|([DdSsPp][Ee])|([Kk][Jj])|([Mm][Aa])|(1[45]))\d{7}$)/
帐号是否合法(字母开头,允许5-16字节,允许字母数字下划线组合
/^[a-zA-Z]\w{4,15}$/
中文/汉字
/^(?:[\u3400-\u4DB5\u4E00-\u9FEA\uFA0E\uFA0F\uFA11\uFA13\uFA14\uFA1F\uFA21\uFA23\uFA24\uFA27-\uFA29]|[\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879][\uDC00-\uDFFF]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0])+$/
小数
/^\d+\.\d+$/
数字
/^\d{1,}$/
html标签(宽松匹配)
/<(\w+)[^>]*>(.*?<\/\1>)?/
qq号格式正确
/^[1-9][0-9]{4,10}$/
数字和字母组成
/^[A-Za-z0-9]+$/
英文字母
/^[a-zA-Z]+$/
小写英文字母组成
/^[a-z]+$/
大写英文字母
/^[A-Z]+$/
密码强度校验,最少6位,包括至少1个大写字母,1个小写字母,1个数字,1个特殊字符
/^\S*(?=\S{6,})(?=\S*\d)(?=\S*[A-Z])(?=\S*[a-z])(?=\S*[!@#$%^&*? ])\S*$/
用户名校验,4到16位(字母,数字,下划线,减号)
/^[a-zA-Z0-9_-]{4,16}$/
ip-v4
/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/
ip-v6
/^((([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}:[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){5}:([0-9A-Fa-f]{1,4}:)?[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){4}:([0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){3}:([0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){2}:([0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(([0-9A-Fa-f]{1,4}:){0,5}:((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(::([0-9A-Fa-f]{1,4}:){0,5}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|([0-9A-Fa-f]{1,4}::([0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4})|(::([0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){1,7}:))$/i
16进制颜色
/^#?([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$/
微信号(wx),6至20位,以字母开头,字母,数字,减号,下划线
/^[a-zA-Z][-_a-zA-Z0-9]{5,19}$/
邮政编码(中国)
/^(0[1-7]|1[0-356]|2[0-7]|3[0-6]|4[0-7]|5[1-7]|6[1-7]|7[0-5]|8[013-6])\d{4}$/
中文和数字
/^((?:[\u3400-\u4DB5\u4E00-\u9FEA\uFA0E\uFA0F\uFA11\uFA13\uFA14\uFA1F\uFA21\uFA23\uFA24\uFA27-\uFA29]|[\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879][\uDC00-\uDFFF]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0])|(\d))+$/
不能包含字母
/^[^A-Za-z]*$/
java包名
/^([a-zA-Z_][a-zA-Z0-9_]*)+([.][a-zA-Z_][a-zA-Z0-9_]*)+$/
mac地址
/^((([a-f0-9]{2}:){5})|(([a-f0-9]{2}-){5}))[a-f0-9]{2}$/i
使用 vue-router 的导航守卫钩子函数,某些钩子函数可以让开发者根据业务逻辑,控制是否进行下一步,或者进入到指定的路由。
例如,后台管理页面,会在进入路由前,进行必要登录、权限判断,来决定去往哪个路由,以下是伪代码:
// 全局导航守卫
router.beforEach((to, from, next) => {
if('no login'){
next('/login')
}else if('admin') {
next('/admin')
}else {
next()
}
})
// 路由配置钩子函数
{
path: '',
component: component,
beforeEnter: (to, from, next) => {
next()
}
}
// 组件中配置钩子函数
{
template: '',
beforeRouteEnter(to, from, next) {
next()
}
}
调用 next,意味着继续进行下面的流程;不调用,则直接终止,导致路由中设置的组件无法渲染,会出现页面一片空白的现象。
钩子函数有不同的作用,例如 beforEach,afterEach,beforeEnter,beforeRouteEnter,beforeRouteUpdate,beforeRouteLeave,针对这些注册的钩子函数,要依次进行执行,并且在必要环节有控制权决定是否继续进入到下一个钩子函数中。
以下分析下源码中实现的方式,而源码中处理的边界情况比较多,需要抓住核心点,去掉冗余代码,精简出便于理解的实现。
精简源码核心功能
总结下核心点:钩子函数注册的回调函数,能顺序执行,同时会将控制权交给开发者。
先来一个能够注册回调函数的类:
class VueRouter {
constructor(){
this.beforeHooks = []
this.beforeEnterHooks = []
this.afterHooks = []
}
beforEach(callback){
return registerHook(this.beforeHooks, callback)
}
beforeEnter(callback){
return registerHook(this.beforeEnterHooks, callback)
}
afterEach(callback){
return registerHook(this.afterHooks, callback)
}
}
function registerHook (list, fn) {
list.push(fn)
return () => {
const i = list.indexOf(fn)
if (i > -1) list.splice(i, 1)
}
}
声明的类,提供了 beforEach 、beforeEnter 和 afterEach 来注册必要的回调函数。
抽象出一个 registerHook 公共方法,作用:
注册回调函数
返回的函数,可以取消注册的回调函数
使用一下:
const router = new VueRouter()
const beforEach = router.beforEach((to, from, next) => {
console.log('beforEach');
next()
})
// 取消注册的函数
beforEach()
以上的回调函数会被取消,意味着不会执行了。
router.beforEach((to, from, next) => {
console.log('beforEach');
next()
})
router.beforeEnter((to, from, next) => {
console.log('beforeEnter');
next()
})
router.afterEach(() => {
console.log('afterEach');
})
以上注册的钩子函数会依次执行。beforEach 和 beforeEnter 的回调接收内部传来的参数,同时通过调用 next 可继续走下面的回调函数,如果不调用,则直接被终止了。
最后一个 afterEach 在上面的回调函数都执行后,才被执行,且不接收任何参数。
先来实现依次执行,这是最简单的方式,在类中增加 run 方法,手动调用:
class VueRouter {
// ... 其他省略,增加 run 函数
run(){
// 把需要依次执行的回调存放在一个队列中
let queue = [].concat(
this.beforeHooks,
this.afterHooks
)
for(let i = 0; i < queue.length; i++){
if(queue(i)) {
queue(i)('to', 'from', () => {})
}
}
}
}
// 手动调用
router.run()
打印:
'beforEach'
'beforeEnter'
上面把要依次执行的回调函数聚合在一个队列中执行,并传入必要的参数,但这样开发者不能控制是否进行下一步,即便不执行 next 函数,依然会依次执行完队列的函数。
改进一下:
class VueRouter {
// ... 其他省略,增加 run 函数
run(){
// 把需要依次执行的回调存放在一个队列中
let queue = [].concat(
this.beforeHooks,
this.afterHooks
)
queue[0]('to', 'from', () => {
queue[1]('to', 'from', () => {
console.log('调用结束');
})
})
}
}
router.beforEach((to, from, next) => {
console.log('beforEach');
// next()
})
router.beforeEnter((to, from, next) => {
console.log('beforeEnter');
next()
})
传入的 next 函数会有调用下一个回调函数的行为,把控制权交给了开发者,调用了 next 函数会继续执行下一个回调函数;不调用 next 函数,则终止了队列的执行,所以打印结果是:
'beforEach'
上面实现有个弊端,代码不够灵活,手动一个个调用,在真实场景中无法确定注册了多少个回调函数,所以需要继续抽象成一个功能更强的方法:
function runQueue (queue, fn, cb) {
const step = index => {
// 队列执行结束了
if (index >= queue.length) {
cb()
} else {
// 队列有值
if (queue[index]) {
// 传入队列中回调,做一些必要的操作,第二个参数是为了进行下一个回调函数
fn(queue[index], () => {
step(index + 1)
})
} else {
step(index + 1)
}
}
}
// 初次调用,从第一个开始
step(0)
}
runQueue 就是执行队列的通用方法。
第一个参数为回调函数队列, 会依次取出来;
第二个参数是函数,它接受队列中的函数,进行一些其他处理;并能进行下个回调函数的执行;
第三个参数是队列执行结束后调用。
知道了这个函数的含义,来使用一下:
class VueRouter {
// ... 其他省略,增加 run 函数
run(){
// 把需要依次执行的回调存放在一个队列中
let queue = [].concat(
this.beforeHooks,
this.beforeEnterHooks
)
// 接收回到函数,和进行下一个的执行函数
const iterator = (hook, next) => {
// 传给回调函数的参数,第三个参数是函数,交给开发者调用,调用后进行下一个
hook('to', 'from', () => {
console.log('执行下一个回调时,处理一些相关信息');
next()
})
}
runQueue(queue, iterator, () => {
console.log('执行结束');
// 执行 afterEach 中的回调函数
this.afterHooks.forEach((fn) => {
fn()
})
})
}
}
// 注册
router.beforEach((to, from, next) => {
console.log('beforEach');
next()
})
router.beforeEnter((to, from, next) => {
console.log('beforeEnter');
next()
})
router.afterEach(() => {
console.log('afterEach');
})
router.run();
从上面代码可以看出来,每次把队列 queue 中的回调函数传给 iterator , 用 hook 接收,并调用。
传给 hook 必要的参数,尤其是第三个参数,开发者在注册的回调函数中调用,来控制进行下一步。
在队列执行完毕后,依次执行 afterHooks 的回调函数,不传入任何参数。
所以打印结果为:
beforEach
执行下一个回调时,处理一些相关信息
beforeEnter
执行下一个回调时,处理一些相关信息
执行结束
afterEach
以上实现的非常巧妙,再看 Vue-router 源码这块的实现方式,相信你会豁然开朗。
蓝蓝设计的小编 http://www.lanlanwork.com