大象无痕

大象无痕 查看完整档案

深圳编辑  |  填写毕业院校深圳网络科技有限公司  |  未来前端架构师 编辑 13189449986.github.io/hexo 编辑
编辑

前端开发啦啦啦

个人动态

大象无痕 收藏了文章 · 2020-12-27

基于 TypeScript + Babel + Rollup 搭建 ts 开发环境

前言

本篇文章讲述了基于 TypeScript + Babel + Rollup 搭建 ts 开发环境的解决方案,先赘述了此方案的优劣势,后按步骤具体讲解各个插件的配置方式,以帮助大家了解 TS 项目的编译原理。

以下是该文章示例代码所依赖的基础环境,如有不符,配置引导可能无法正常运行。

TypeScript + Babel 方案的特点

什么是 Babel-TypeScript

大家也许在 Babel 官网中看到过一个 preset,叫 @babel/preset-typescript,没错,Babel 不仅可以编译 ES6 的语法糖、对浏览器兼容性进行 polyfill 以外,还扩展了对 TypeScript 的支持,但它与官方的 TypeScript 有一些差异,比较特殊。

下面,由我来给大家一一道来。

开发体验特点

使用 TypeScript + Babel 方案搭建的项目有什么特点?

  1. 开发编译过程,babel 会直接去除 typescript 类型标记,输出编译结果

    1. 速度很快,但没有类型检查
  2. 通过 eslint 做语法检查和类型检查

    1. 并且检查时机是在 commit 时(由 husky 添加的钩子)
    2. 当然,vscode 对 typescript 的类型检查也有一定程度的支持
  3. 为了降低语法检查带来的麻烦,通过配置 prettier 来格式化代码

    1. 保存文件时(需要配置编辑器)触发 prettier
    2. 执行 git commit 时(由 husky 实现)触发 prettier

Babel 编译的劣势

万事开头难,先说说 Babel 编译的劣势

  1. Babel 编译 TypeScript 不会做类型检查,所有的类型声明都会被 Babel 抛弃掉
  2. 不支持编译部分语法,比如:import xx = ‘xx’、namespace(无关紧要,这些语法都已弃用了)

Typescript 编译器完全没有上述的问题,那为什么还要多此一举使用 Babel 编译 Typescript 呢?

Babel 编译的优势

先苦后甜,再来说说 Babel 的优势

  1. 灵活性 - Babel 支持根据浏览器兼容性要求按需编译,这个 TypeScript 是不支持的,且官方也声明过不在考虑范围内
  2. Polyfill - Babel 支持根据浏览器兼容性要求按需添加 Polyfill
  3. Plugins/Presets - Babel 支持超级多的 Plugins,而且通过预设 Presets 可以免去复杂的 Plugins 配置,这点 TypeScript 也不能满足

好,赘述完了,我们来看一下实战演练,动手做个 demo。

懒人请执行以下命令,后面的就不用动手了(先查看上面的基础环境要求,以确保你的环境可以正常运行以下命令)

git clone https://github.com/lianer/test-babel-typescript.git

创建一个空的 demo 项目

在命令行直接执行这些命令,先搭一个基础的仓库脚手架

mkdir test
cd test

npm init

mkdir src
cd src

# 下面多行内容一整段复制执行
cat <<EOF > index.ts
const sum = function (a: number, b: number): number {
  return a + b;
};
console.log(sum(1, 2));
EOF

目录结构如下

| src
  | index.ts
| package.json

配置 typescript

配置核心要求的 typescript,虽然仅仅会在 lint 的时候用到它

1、安装 typescript 依赖
yarn add -D typescript
2、添加 tsconfig 配置文件

因为是 demo 项目,没有太多要求,因此这里保持默认的配置就足够了

./node_modules/.bin/tsc --init

配置 rollup、babel 套件

支持对 ts 文件的编译,产出 js 文件

相关资料

配置引导

1、安装相关依赖
# 安装 rollup 套件
yarn add -D rollup rollup-plugin-babel@latest @rollup/plugin-node-resolve

# 安装 babel 套件
yarn add -D @babel/core @babel/preset-env @babel/preset-typescript
2、添加 .babelrc 配置文件
{
  "presets": [
    "@babel/preset-env",
    "@babel/preset-typescript"
  ]
}
3、添加 rollup.config.js 配置文件
const path = require('path');
const babel = require('rollup-plugin-babel');
const nodeResolve = require('@rollup/plugin-node-resolve');
const pkg = require('./package.json');

const extensions = ['.js', '.ts'];

const resolve = function(...args) {
  return path.resolve(__dirname, ...args);
};

module.exports = {
  input: resolve('./src/index.ts'),
  output: {
    file: resolve('./', pkg.main), // 为了项目的统一性,这里读取 package.json 中的配置项
    format: 'esm',
  },
  plugins: [
    nodeResolve({
      extensions,
      modulesOnly: true,
    }),
    babel({
      exclude: 'node_modules/**',
      extensions,
    }),
  ],
};
4、配置 npm scripts

如果先前项目里还没有 package.json 文件,可以先通过 npm init 命令初始化一个

"scripts": {
  "build": "rollup -c"
},
5、在 rollup.config.js 中,用到了 package.json - main 配置项,因此别忘了修改一下 package.json
"main": "lib/index.js"
5、测试一下
npm run build

打包成功,输出结果

> test-typescript-babel@1.0.0 build /dev/test
/dev/test/src/index.ts → lib/index.js...
> rollup -c
created lib/index.js in 460ms

目录结构

| lib
  | index.js
| src
  | index.ts
| .babelrc
| package.json
| rollup.config.js
| tsconfig.json
| yarn.lock

编译出的 lib/index.js 文件内容

var sum = function sum(a, b) {
  return a + b;
};

console.log(sum(1, 2));

认真的同学可能注意到了,Babel 仅仅只是把 TypeScript 类型移除了而已。

注:如果你再这一步报错了,提示 The "path" argument must be of type string. Received type undefined,则检查 package.json - main 配置是否有误

配置 eslint、prettier

支持代码的类型校验、语法校验,以及代码格式化

相关资料

配置引导

1、安装依赖
# 安装 eslint 套件
yarn add -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin

# 安装 prettier
yarn add -D prettier

# 安装 husky、lint-staged 套件
yanr add -D husky lint-staged
2、添加配置文件 .eslintrc.js

.eslintrc.js 文件描述 eslint 语法检查和 ts 类型检查的规则

const path = require('path');
module.exports = {
  parser: '@typescript-eslint/parser',
  extends: [
    'plugin:@typescript-eslint/recommended' // Uses the recommended rules from the @typescript-eslint/eslint-plugin
  ],
  parserOptions: {
    project: path.resolve(__dirname, './tsconfig.json'),
    tsconfigRootDir: __dirname,
    ecmaVersion: 2019, // Allows for the parsing of modern ECMAScript features
    sourceType: 'module', // Allows for the use of imports
  },
  rules: {
    // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs
    // e.g. "@typescript-eslint/explicit-function-return-type": "off",
  }
};
3、添加配置文件 .prettierrc

.prettierrc 文件描述代码格式化的规则

{
  "semi":  true,
  "trailingComma":  "all",
  "singleQuote":  true,
  "printWidth":  120,
  "tabWidth":  2
}
4、修改 package.json,配置 husky 和 lint-staged

一个非常庞大的项目,eslint 完整检查可能需要花费几分钟的时间。

而 husky + lint-staged 可以实现只对提交的文件进行检查,从而提升开发效率。

这样即使你的项目再大,也仅仅是检查本次提交的文件,只需几秒钟。

"scripts": {
  "lint": "eslint 'src/**/*.{js,ts}'"
},
"husky": {
  "hooks": {
    "pre-commit": "lint-staged"
  }
},
"lint-staged": {
  "*./src/**/*.{js,ts,json,css,less,md}": [
    "prettier --write",
    "yarn lint"
  ]
}

上述配置可以实现在执行 git commit 时调用 prettier 格式化代码,并使用 eslint 做类型和语法的检查。

5、如果想要保存文件时自动格式化代码,则需要安装 vscode 插件 prettier,并做如下配置
"editor.formatOnSave": false, // 全局默认关闭不做格式化,仅针对 js 和 ts 格式化
"[javascript]": {
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.formatOnSave": true,
},
"[typescript]": {
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.formatOnSave": true,
},

进一步完善打包机制

进一步完善 rollup 的打包机制,支持多任务打包,打包出不同项目所需的资源文件。

引入 rimraf, npm-run-all, rollup-plugin-uglify, lodash.merge

相关资料

  • rimraf - 文件删除工具,用于每次编译前清空 lib 目录
  • npm-run-all - npm 命令并行执行工具
  • rollup-plugin-uglify - uglify js 压缩工具(rollup 版)
  • lodash.merge - 配置合并工具

配置引导

1、 安装依赖
yarn add -D rimraf npm-run-all rollup-plugin-uglify lodash.merge
2、修改 rollup.config.js
const path = require('path');
const babel = require('rollup-plugin-babel');
const nodeResolve = require('@rollup/plugin-node-resolve');
const uglify = require('rollup-plugin-uglify').uglify;
const merge = require('lodash.merge');
const pkg = require('./package.json');

const extensions = ['.js', '.ts'];

const resolve = function(...args) {
  return path.resolve(__dirname, ...args);
};

// 打包任务的个性化配置
const jobs = {
  esm: {
    output: {
      format: 'esm',
      file: resolve(pkg.module),
    },
  },
  umd: {
    output: {
      format: 'umd',
      file: resolve(pkg.main),
      name: 'rem',
    },
  },
  min: {
    output: {
      format: 'umd',
      file: resolve(pkg.main.replace(/(.\w+)$/, '.min$1')),
      name: 'rem',
    },
    plugins: [uglify()],
  },
};

// 从环境变量获取打包特征
const mergeConfig = jobs[process.env.FORMAT || 'esm'];

module.exports = merge(
  {
    input: resolve('./src/index.ts'),
    output: {},
    plugins: [
      nodeResolve({
        extensions,
        modulesOnly: true,
      }),
      babel({
        exclude: 'node_modules/**',
        extensions,
      }),
    ],
  },
  mergeConfig,
);
3、修改 package.json - scripts
"main": "lib/index.umd.js",
"module": "lib/index.esm.js",
"scripts": {
  "lint": "eslint 'src/**/*.{js,ts}'",
  "dev": "rollup -w -c --environment FORMAT:esm",
  "build:esm": "rollup -c --environment FORMAT:esm",
  "build:umd": "rollup -c --environment FORMAT:umd",
  "build:min": "rollup -c --environment FORMAT:min",
  "build": "rimraf lib/* && run-p build:esm build:umd build:min"
},

在这里,我们先通过 rimraf 工具清空 lib 目录,然后再通过 npm-run-all 工具并行 3 个子编译任务

这里的 3 个子编译任务,分别是:

  • build:esm - 编译出符合 esm 规范的可执行文件,供 Vue、React 等采用 esmodule 规范进行模块化打包的项目使用
  • build:umd - 编译出符合 umd 规范的可执行文件,供 jQuery、Vue、NodeJS 等项目使用
  • build:min - 编译出符合 umd 规范的压缩的可执行文件
4、测试执行打包命令
npm run build

输出结果文件

| lib
  | rem.esm.js
  | rem.umd.js
  | rem.umd.min.js

进一步完善工程

统一编辑器的行为,比如空格、缩进等,并添加 git 忽略列表

1、添加 .editorconfig 配置文件,编辑器安装相应的 editorconfig 插件,使该项目统一应用相同的空格、缩进等编码风格
root = true

[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
2、添加 .gitignore 配置文件,这里推荐一个 .gitignore 的生成工具 - gitignore.io
# Created by https://www.gitignore.io/api/vuejs,visualstudiocode
# Edit at https://www.gitignore.io/?templates=vuejs,visualstudiocode

### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json

### VisualStudioCode Patch ###
# Ignore all local history of files
.history

### Vuejs ###
# Recommended template: Node.gitignore

node_modules/
dist/
npm-debug.log
yarn-error.log

# End of https://www.gitignore.io/api/vuejs,visualstudiocode

结尾

至此,基于 Rollup + Babel + TypeScript + ESLint 的整套套件的搭建就完成了。

如果有不够完善的地方,欢迎在文末进行点评[点赞]。

查看原文

大象无痕 赞了文章 · 2020-12-27

基于 TypeScript + Babel + Rollup 搭建 ts 开发环境

前言

本篇文章讲述了基于 TypeScript + Babel + Rollup 搭建 ts 开发环境的解决方案,先赘述了此方案的优劣势,后按步骤具体讲解各个插件的配置方式,以帮助大家了解 TS 项目的编译原理。

以下是该文章示例代码所依赖的基础环境,如有不符,配置引导可能无法正常运行。

TypeScript + Babel 方案的特点

什么是 Babel-TypeScript

大家也许在 Babel 官网中看到过一个 preset,叫 @babel/preset-typescript,没错,Babel 不仅可以编译 ES6 的语法糖、对浏览器兼容性进行 polyfill 以外,还扩展了对 TypeScript 的支持,但它与官方的 TypeScript 有一些差异,比较特殊。

下面,由我来给大家一一道来。

开发体验特点

使用 TypeScript + Babel 方案搭建的项目有什么特点?

  1. 开发编译过程,babel 会直接去除 typescript 类型标记,输出编译结果

    1. 速度很快,但没有类型检查
  2. 通过 eslint 做语法检查和类型检查

    1. 并且检查时机是在 commit 时(由 husky 添加的钩子)
    2. 当然,vscode 对 typescript 的类型检查也有一定程度的支持
  3. 为了降低语法检查带来的麻烦,通过配置 prettier 来格式化代码

    1. 保存文件时(需要配置编辑器)触发 prettier
    2. 执行 git commit 时(由 husky 实现)触发 prettier

Babel 编译的劣势

万事开头难,先说说 Babel 编译的劣势

  1. Babel 编译 TypeScript 不会做类型检查,所有的类型声明都会被 Babel 抛弃掉
  2. 不支持编译部分语法,比如:import xx = ‘xx’、namespace(无关紧要,这些语法都已弃用了)

Typescript 编译器完全没有上述的问题,那为什么还要多此一举使用 Babel 编译 Typescript 呢?

Babel 编译的优势

先苦后甜,再来说说 Babel 的优势

  1. 灵活性 - Babel 支持根据浏览器兼容性要求按需编译,这个 TypeScript 是不支持的,且官方也声明过不在考虑范围内
  2. Polyfill - Babel 支持根据浏览器兼容性要求按需添加 Polyfill
  3. Plugins/Presets - Babel 支持超级多的 Plugins,而且通过预设 Presets 可以免去复杂的 Plugins 配置,这点 TypeScript 也不能满足

好,赘述完了,我们来看一下实战演练,动手做个 demo。

懒人请执行以下命令,后面的就不用动手了(先查看上面的基础环境要求,以确保你的环境可以正常运行以下命令)

git clone https://github.com/lianer/test-babel-typescript.git

创建一个空的 demo 项目

在命令行直接执行这些命令,先搭一个基础的仓库脚手架

mkdir test
cd test

npm init

mkdir src
cd src

# 下面多行内容一整段复制执行
cat <<EOF > index.ts
const sum = function (a: number, b: number): number {
  return a + b;
};
console.log(sum(1, 2));
EOF

目录结构如下

| src
  | index.ts
| package.json

配置 typescript

配置核心要求的 typescript,虽然仅仅会在 lint 的时候用到它

1、安装 typescript 依赖
yarn add -D typescript
2、添加 tsconfig 配置文件

因为是 demo 项目,没有太多要求,因此这里保持默认的配置就足够了

./node_modules/.bin/tsc --init

配置 rollup、babel 套件

支持对 ts 文件的编译,产出 js 文件

相关资料

配置引导

1、安装相关依赖
# 安装 rollup 套件
yarn add -D rollup rollup-plugin-babel@latest @rollup/plugin-node-resolve

# 安装 babel 套件
yarn add -D @babel/core @babel/preset-env @babel/preset-typescript
2、添加 .babelrc 配置文件
{
  "presets": [
    "@babel/preset-env",
    "@babel/preset-typescript"
  ]
}
3、添加 rollup.config.js 配置文件
const path = require('path');
const babel = require('rollup-plugin-babel');
const nodeResolve = require('@rollup/plugin-node-resolve');
const pkg = require('./package.json');

const extensions = ['.js', '.ts'];

const resolve = function(...args) {
  return path.resolve(__dirname, ...args);
};

module.exports = {
  input: resolve('./src/index.ts'),
  output: {
    file: resolve('./', pkg.main), // 为了项目的统一性,这里读取 package.json 中的配置项
    format: 'esm',
  },
  plugins: [
    nodeResolve({
      extensions,
      modulesOnly: true,
    }),
    babel({
      exclude: 'node_modules/**',
      extensions,
    }),
  ],
};
4、配置 npm scripts

如果先前项目里还没有 package.json 文件,可以先通过 npm init 命令初始化一个

"scripts": {
  "build": "rollup -c"
},
5、在 rollup.config.js 中,用到了 package.json - main 配置项,因此别忘了修改一下 package.json
"main": "lib/index.js"
5、测试一下
npm run build

打包成功,输出结果

> test-typescript-babel@1.0.0 build /dev/test
/dev/test/src/index.ts → lib/index.js...
> rollup -c
created lib/index.js in 460ms

目录结构

| lib
  | index.js
| src
  | index.ts
| .babelrc
| package.json
| rollup.config.js
| tsconfig.json
| yarn.lock

编译出的 lib/index.js 文件内容

var sum = function sum(a, b) {
  return a + b;
};

console.log(sum(1, 2));

认真的同学可能注意到了,Babel 仅仅只是把 TypeScript 类型移除了而已。

注:如果你再这一步报错了,提示 The "path" argument must be of type string. Received type undefined,则检查 package.json - main 配置是否有误

配置 eslint、prettier

支持代码的类型校验、语法校验,以及代码格式化

相关资料

配置引导

1、安装依赖
# 安装 eslint 套件
yarn add -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin

# 安装 prettier
yarn add -D prettier

# 安装 husky、lint-staged 套件
yanr add -D husky lint-staged
2、添加配置文件 .eslintrc.js

.eslintrc.js 文件描述 eslint 语法检查和 ts 类型检查的规则

const path = require('path');
module.exports = {
  parser: '@typescript-eslint/parser',
  extends: [
    'plugin:@typescript-eslint/recommended' // Uses the recommended rules from the @typescript-eslint/eslint-plugin
  ],
  parserOptions: {
    project: path.resolve(__dirname, './tsconfig.json'),
    tsconfigRootDir: __dirname,
    ecmaVersion: 2019, // Allows for the parsing of modern ECMAScript features
    sourceType: 'module', // Allows for the use of imports
  },
  rules: {
    // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs
    // e.g. "@typescript-eslint/explicit-function-return-type": "off",
  }
};
3、添加配置文件 .prettierrc

.prettierrc 文件描述代码格式化的规则

{
  "semi":  true,
  "trailingComma":  "all",
  "singleQuote":  true,
  "printWidth":  120,
  "tabWidth":  2
}
4、修改 package.json,配置 husky 和 lint-staged

一个非常庞大的项目,eslint 完整检查可能需要花费几分钟的时间。

而 husky + lint-staged 可以实现只对提交的文件进行检查,从而提升开发效率。

这样即使你的项目再大,也仅仅是检查本次提交的文件,只需几秒钟。

"scripts": {
  "lint": "eslint 'src/**/*.{js,ts}'"
},
"husky": {
  "hooks": {
    "pre-commit": "lint-staged"
  }
},
"lint-staged": {
  "*./src/**/*.{js,ts,json,css,less,md}": [
    "prettier --write",
    "yarn lint"
  ]
}

上述配置可以实现在执行 git commit 时调用 prettier 格式化代码,并使用 eslint 做类型和语法的检查。

5、如果想要保存文件时自动格式化代码,则需要安装 vscode 插件 prettier,并做如下配置
"editor.formatOnSave": false, // 全局默认关闭不做格式化,仅针对 js 和 ts 格式化
"[javascript]": {
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.formatOnSave": true,
},
"[typescript]": {
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.formatOnSave": true,
},

进一步完善打包机制

进一步完善 rollup 的打包机制,支持多任务打包,打包出不同项目所需的资源文件。

引入 rimraf, npm-run-all, rollup-plugin-uglify, lodash.merge

相关资料

  • rimraf - 文件删除工具,用于每次编译前清空 lib 目录
  • npm-run-all - npm 命令并行执行工具
  • rollup-plugin-uglify - uglify js 压缩工具(rollup 版)
  • lodash.merge - 配置合并工具

配置引导

1、 安装依赖
yarn add -D rimraf npm-run-all rollup-plugin-uglify lodash.merge
2、修改 rollup.config.js
const path = require('path');
const babel = require('rollup-plugin-babel');
const nodeResolve = require('@rollup/plugin-node-resolve');
const uglify = require('rollup-plugin-uglify').uglify;
const merge = require('lodash.merge');
const pkg = require('./package.json');

const extensions = ['.js', '.ts'];

const resolve = function(...args) {
  return path.resolve(__dirname, ...args);
};

// 打包任务的个性化配置
const jobs = {
  esm: {
    output: {
      format: 'esm',
      file: resolve(pkg.module),
    },
  },
  umd: {
    output: {
      format: 'umd',
      file: resolve(pkg.main),
      name: 'rem',
    },
  },
  min: {
    output: {
      format: 'umd',
      file: resolve(pkg.main.replace(/(.\w+)$/, '.min$1')),
      name: 'rem',
    },
    plugins: [uglify()],
  },
};

// 从环境变量获取打包特征
const mergeConfig = jobs[process.env.FORMAT || 'esm'];

module.exports = merge(
  {
    input: resolve('./src/index.ts'),
    output: {},
    plugins: [
      nodeResolve({
        extensions,
        modulesOnly: true,
      }),
      babel({
        exclude: 'node_modules/**',
        extensions,
      }),
    ],
  },
  mergeConfig,
);
3、修改 package.json - scripts
"main": "lib/index.umd.js",
"module": "lib/index.esm.js",
"scripts": {
  "lint": "eslint 'src/**/*.{js,ts}'",
  "dev": "rollup -w -c --environment FORMAT:esm",
  "build:esm": "rollup -c --environment FORMAT:esm",
  "build:umd": "rollup -c --environment FORMAT:umd",
  "build:min": "rollup -c --environment FORMAT:min",
  "build": "rimraf lib/* && run-p build:esm build:umd build:min"
},

在这里,我们先通过 rimraf 工具清空 lib 目录,然后再通过 npm-run-all 工具并行 3 个子编译任务

这里的 3 个子编译任务,分别是:

  • build:esm - 编译出符合 esm 规范的可执行文件,供 Vue、React 等采用 esmodule 规范进行模块化打包的项目使用
  • build:umd - 编译出符合 umd 规范的可执行文件,供 jQuery、Vue、NodeJS 等项目使用
  • build:min - 编译出符合 umd 规范的压缩的可执行文件
4、测试执行打包命令
npm run build

输出结果文件

| lib
  | rem.esm.js
  | rem.umd.js
  | rem.umd.min.js

进一步完善工程

统一编辑器的行为,比如空格、缩进等,并添加 git 忽略列表

1、添加 .editorconfig 配置文件,编辑器安装相应的 editorconfig 插件,使该项目统一应用相同的空格、缩进等编码风格
root = true

[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
2、添加 .gitignore 配置文件,这里推荐一个 .gitignore 的生成工具 - gitignore.io
# Created by https://www.gitignore.io/api/vuejs,visualstudiocode
# Edit at https://www.gitignore.io/?templates=vuejs,visualstudiocode

### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json

### VisualStudioCode Patch ###
# Ignore all local history of files
.history

### Vuejs ###
# Recommended template: Node.gitignore

node_modules/
dist/
npm-debug.log
yarn-error.log

# End of https://www.gitignore.io/api/vuejs,visualstudiocode

结尾

至此,基于 Rollup + Babel + TypeScript + ESLint 的整套套件的搭建就完成了。

如果有不够完善的地方,欢迎在文末进行点评[点赞]。

查看原文

赞 28 收藏 12 评论 5

大象无痕 关注了用户 · 2020-11-25

accord @accord

希望遇到一个公司,遇到一个团队,大家都愿意把code当作一种艺术去书写

关注 54

大象无痕 关注了专栏 · 2020-11-03

JSer

分享自己在前端道路上所学到的一切,让自己的思想和大家碰撞出新的火花。

关注 133

大象无痕 收藏了文章 · 2020-08-06

IntersectionObserver滚动加载

IntersectionObserver

定义

就是监听根元素与目标元素在指定的交叉比例时触发回调

兼容性

  • 兼容部分现代游览器(chrome)生产环境中使用,请注意兼容需求

使用场景

  • 在一定使用场景可替代onscroll
    1、写法上更加简单优雅
    2、避免了onscroll带来的大量位置计算,提高性能
  • 例如1:滚动(上拉)加载
  • 例如2: 懒加载

API介绍

创建一个实例
var observer = new IntersectionObserver(callback,options);
参数

callback:指定比例交叉时触发的回调

 new IntersectionObserver((entries) => { 
 },options)
  • entries 描述交叉状态,具体可见 entries

options:配置参数

字段类型默认值描述
rootelementnull(默认为浏览器视窗)   根元素
rootMargin   string0root外边距,类似margin可为负数('10px -10px 10px -10px')
thresholdnumber/ary   0触发回调的交叉比例
方法
  • IntersectionObserverEntry.boundingClientRect 使IntersectionObserver对象停止监听工作
  • IntersectionObserver.observe() 对象的observe() 方法向, 但是可以监视多个目标元素,以查看这些目标元素可见区域的变化。
  • IntersectionObserver.takeRecords() 返回所有观察目标的IntersectionObserverEntry对象数组
  • IntersectionObserver.unobserve()使IntersectionObserver停止监听特定目标元素。

加载更多实现(原生)

index.html

  • 设置一个固定元素作为哨兵
<body>
  <div id="content" class="box">

    <ul id="list" class="list">
      <!--  数据加载位置 -->
    </ul>

    <!-- 加载触发守卫 -->
    <div id="bottom" class="bottom">...LoadingMore....</div>
  </div>
</body>

index.js

class LoadMore {
  constructor(options) {
    this.observer = null
    this.rootNode = document.getElementById(options.root) // 父级容器
    this.obsNode = document.getElementById(options.obs) // 守卫节点
    this.loadMoreFn = options.loadMoreFn // 加载回调
    this.pageNum = 0 // 记录页数
    this.total = -1 // 总页数 -1 // 尚未获取
    this.init() // 初始化
  }
  // 交叉触发回调
  callback = entries => {
    console.log(entries)
    // 防止插入瞬间再次触发离开交叉事件
    if (entries[0].intersectionRatio === 0) return
    this.pageNum += 1
    this.loadMoreFn(this.pageNum)
  }
  // 关闭交叉监听
  canclobserve() {
    console.log('完毕')
    this.observer.unobserve(this.obsNode)
  }
  // 设置首位节点交叉监听
  hanldeObserve() {
    this.observer.observe(this.obsNode)
  }
  // 初始化
  init() {
    // 创建 IntersectionObserver 实例子
    this.observer = new IntersectionObserver(this.callback, {
      root: this.rootNode || null, // 交叉窗口、根元素
      rootMargin: '0px', // 交叉窗口边界扩展或者伸缩
      threshold: [0.8] // 交叉占比(0~1),达到该比例才触发回调
    })
    this.hanldeObserve()
  }
}
  • 当回调调用插入元素,会将哨兵元素基础root,此时会再次触发交叉回调 if (entries[0].intersectionRatio === 0) return 通过此来防止这种情况

调用

let loadEx = new LoadMore({
  root: 'content',
  obs: 'bottom',
  loadMoreFn: pageNum => {
    //  最好一页关闭监听
    if (pageNum * 10 > list.length) {
      loadEx.canclobserve()
    }
    setTimeout(() => {
      // 插入dom
      let curPage = list.slice((pageNum - 1) * 10, pageNum * 10)
      // 创建文档碎片
      let frm = document.createDocumentFragment()
      for (let i = 0; i < curPage.length; i++) {
        let li = document.createElement('li')
        li.innerHTML = curPage[i]
        li.className = 'item-list'
        frm.appendChild(li)
      }
      document.getElementById('list').appendChild(frm)
      frm = null
    }, Math.random() * 3000)
  }
})

vue 自定义指令实现

Vue.directive('loadMore', {
  bind: function(el, binding) {
    let opation = binding.value
    let div = document.createElement('div')
    div.id = 'footGuard'
    div.style.width = '100%'
    div.style.height = '20px'
    el.appendChild(div)
    el.observer = new IntersectionObserver(
      entries => {
        if (entries[0].intersectionRatio === 0) return
        opation()
      },
      {
        root: el || null, // 交叉窗口、根元素
        rootMargin: '100px', // 交叉窗口边界扩展或者伸缩
        threshold: 0.5 // 交叉占比(0~1),达到该比例才触发回调
      }
    )
    el.observer.observe(div)
  },
  inserted: function() {},
  update: function() {},
  componentUpdated: function() {},
  unbind: function() {}
})

组件内使用

<template>
  <div class='content'
       v-loadMore="queryList">
    <ul class="load-more-list">
      <li class="load-more-item"
          v-for="item in list"
          :key="item.id">{{item.name}}</li>
    </ul>
  </div>
</template>

<script>


let createList = function (index) {
  let ary = []
  for (let i = index; i < index + 10; i++) {
    ary.push({
      id: i,
      name: 'index' + i
    })
  }
  return ary
}

export default {
  data () {
    return {
      list: []
    };
  },
  //方法集合
  methods: {
    queryList: function () {
      setTimeout(() => {
        this.list = this.list.concat(createList(this.list.length - 1))
      }, (Math.random() + 1) * 1000)
    }
  },
  // 生命周期 - 挂载完成(可以访问DOM元素)
  mounted () {
    this.queryList()
  }
}
</script>
<style  scoped>

参考文档

IntersectionObserver

查看原文

大象无痕 赞了文章 · 2020-08-06

IntersectionObserver滚动加载

IntersectionObserver

定义

就是监听根元素与目标元素在指定的交叉比例时触发回调

兼容性

  • 兼容部分现代游览器(chrome)生产环境中使用,请注意兼容需求

使用场景

  • 在一定使用场景可替代onscroll
    1、写法上更加简单优雅
    2、避免了onscroll带来的大量位置计算,提高性能
  • 例如1:滚动(上拉)加载
  • 例如2: 懒加载

API介绍

创建一个实例
var observer = new IntersectionObserver(callback,options);
参数

callback:指定比例交叉时触发的回调

 new IntersectionObserver((entries) => { 
 },options)
  • entries 描述交叉状态,具体可见 entries

options:配置参数

字段类型默认值描述
rootelementnull(默认为浏览器视窗)   根元素
rootMargin   string0root外边距,类似margin可为负数('10px -10px 10px -10px')
thresholdnumber/ary   0触发回调的交叉比例
方法
  • IntersectionObserverEntry.boundingClientRect 使IntersectionObserver对象停止监听工作
  • IntersectionObserver.observe() 对象的observe() 方法向, 但是可以监视多个目标元素,以查看这些目标元素可见区域的变化。
  • IntersectionObserver.takeRecords() 返回所有观察目标的IntersectionObserverEntry对象数组
  • IntersectionObserver.unobserve()使IntersectionObserver停止监听特定目标元素。

加载更多实现(原生)

index.html

  • 设置一个固定元素作为哨兵
<body>
  <div id="content" class="box">

    <ul id="list" class="list">
      <!--  数据加载位置 -->
    </ul>

    <!-- 加载触发守卫 -->
    <div id="bottom" class="bottom">...LoadingMore....</div>
  </div>
</body>

index.js

class LoadMore {
  constructor(options) {
    this.observer = null
    this.rootNode = document.getElementById(options.root) // 父级容器
    this.obsNode = document.getElementById(options.obs) // 守卫节点
    this.loadMoreFn = options.loadMoreFn // 加载回调
    this.pageNum = 0 // 记录页数
    this.total = -1 // 总页数 -1 // 尚未获取
    this.init() // 初始化
  }
  // 交叉触发回调
  callback = entries => {
    console.log(entries)
    // 防止插入瞬间再次触发离开交叉事件
    if (entries[0].intersectionRatio === 0) return
    this.pageNum += 1
    this.loadMoreFn(this.pageNum)
  }
  // 关闭交叉监听
  canclobserve() {
    console.log('完毕')
    this.observer.unobserve(this.obsNode)
  }
  // 设置首位节点交叉监听
  hanldeObserve() {
    this.observer.observe(this.obsNode)
  }
  // 初始化
  init() {
    // 创建 IntersectionObserver 实例子
    this.observer = new IntersectionObserver(this.callback, {
      root: this.rootNode || null, // 交叉窗口、根元素
      rootMargin: '0px', // 交叉窗口边界扩展或者伸缩
      threshold: [0.8] // 交叉占比(0~1),达到该比例才触发回调
    })
    this.hanldeObserve()
  }
}
  • 当回调调用插入元素,会将哨兵元素基础root,此时会再次触发交叉回调 if (entries[0].intersectionRatio === 0) return 通过此来防止这种情况

调用

let loadEx = new LoadMore({
  root: 'content',
  obs: 'bottom',
  loadMoreFn: pageNum => {
    //  最好一页关闭监听
    if (pageNum * 10 > list.length) {
      loadEx.canclobserve()
    }
    setTimeout(() => {
      // 插入dom
      let curPage = list.slice((pageNum - 1) * 10, pageNum * 10)
      // 创建文档碎片
      let frm = document.createDocumentFragment()
      for (let i = 0; i < curPage.length; i++) {
        let li = document.createElement('li')
        li.innerHTML = curPage[i]
        li.className = 'item-list'
        frm.appendChild(li)
      }
      document.getElementById('list').appendChild(frm)
      frm = null
    }, Math.random() * 3000)
  }
})

vue 自定义指令实现

Vue.directive('loadMore', {
  bind: function(el, binding) {
    let opation = binding.value
    let div = document.createElement('div')
    div.id = 'footGuard'
    div.style.width = '100%'
    div.style.height = '20px'
    el.appendChild(div)
    el.observer = new IntersectionObserver(
      entries => {
        if (entries[0].intersectionRatio === 0) return
        opation()
      },
      {
        root: el || null, // 交叉窗口、根元素
        rootMargin: '100px', // 交叉窗口边界扩展或者伸缩
        threshold: 0.5 // 交叉占比(0~1),达到该比例才触发回调
      }
    )
    el.observer.observe(div)
  },
  inserted: function() {},
  update: function() {},
  componentUpdated: function() {},
  unbind: function() {}
})

组件内使用

<template>
  <div class='content'
       v-loadMore="queryList">
    <ul class="load-more-list">
      <li class="load-more-item"
          v-for="item in list"
          :key="item.id">{{item.name}}</li>
    </ul>
  </div>
</template>

<script>


let createList = function (index) {
  let ary = []
  for (let i = index; i < index + 10; i++) {
    ary.push({
      id: i,
      name: 'index' + i
    })
  }
  return ary
}

export default {
  data () {
    return {
      list: []
    };
  },
  //方法集合
  methods: {
    queryList: function () {
      setTimeout(() => {
        this.list = this.list.concat(createList(this.list.length - 1))
      }, (Math.random() + 1) * 1000)
    }
  },
  // 生命周期 - 挂载完成(可以访问DOM元素)
  mounted () {
    this.queryList()
  }
}
</script>
<style  scoped>

参考文档

IntersectionObserver

查看原文

赞 10 收藏 8 评论 0

大象无痕 收藏了文章 · 2020-08-01

记一次思否问答的问题思考:Vue为什么不能检测数组变动

问题来源:https://segmentfault.com/q/10...

问题描述:Vue检测数据的变动是通过Object.defineProperty实现的,所以无法监听数组的添加操作是可以理解的,因为是在构造函数中就已经为所有属性做了这个检测绑定操作。

但是官方的原文:由于 JavaScript 的限制, Vue 不能检测以下变动的数组:

当你利用索引直接设置一个项时,例如: vm.items[indexOfItem] = newValue
当你修改数组的长度时,例如: vm.items.length = newLength

这句话是什么意思?我测试了下Object.defineProperty是可以通过索引属性来设置属性的访问器属性的,那为何做不了监听?

有些论坛上的人说因为数组长度是可变的,即使长度为5,但是未必有索引4,我就想问问这个答案哪里来的,修改length,新增的元素会被添加到最后,它的值为undefined,通过索引一样可以获取他们的值,怎么就叫做“未必有索引4”了呢?

既然知道数组的长度为何不能遍历所有元素并通过索引这个属性全部添加set和get不就可以同时更新视图了吗?

如果非要说的话,考虑到性能的问题,假设元素内容只有4个有意义的值,但是长度确实1000,我们不可能为1000个元素做检测操作。但是官方说的由于JS限制,我想知道这个限制是什么内容?各位大大帮我解决下这个问题,感谢万分



面对这个问题,我想说的是,首先,长度为1000,但只有4个元素的数组并不一定会影响性能,因为js中对数据的遍历除了for循环还有forEach、map、filter、some等,除了for循环外(for,for...of),其他的遍历都是对键值的遍历,也就是除了那四个元素外的空位并不会进行遍历(执行回调),所以也就不会造成性能损耗,因为循环体中没有操作的话,所带来的性能影响可以忽略不计,下面是长度为10000,但只有两个元素的数组分别使用for及forEach遍历的结果:

var arr = [1]; arr[10000] = 1
function a(){
    console.time()
    for(var i = 0;i<arr.length;i++)console.log(1)
    console.timeEnd()
}
a(); //default: 567.1669921875ms
a(); //default: 566.2451171875ms

function b(){
    console.time()
    arr.forEach(item=>{console.log(2)})
    console.timeEnd()
}
b(); //default: 0.81982421875ms
b(); //default: 0.434814453125ms

可以看到结果非常明显,不过,如果for循环中不做操作的话两者速度差不多

其次,我要说的是,我也不知道这个限制是什么      (⇀‸↼‶)      ╮( •́ω•̀ )╭

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。数组的索引也是属性,所以我们是可以监听到数组元素的变化的

var arr = [1,2,3,4]
arr.forEach((item,index)=>{
    Object.defineProperty(arr,index,{
        set:function(val){
            console.log('set')
            item = val
        },
        get:function(val){
            console.log('get')
            return item
        }
    })
})
arr[1]; // get  2
arr[1] = 1; // set  1

但是我们新增一个元素,就不会触发监听事件,因为这个新属性我们并没有监听,删除一个属性也是。

再回到题主的问题,既然数组是可以被监听的,那为什么vue不能检测vm.items[indexOfItem] = newValue导致的数组元素改变呢,哪怕这个下标所对应的元素是存在的,且被监听了的?

为了搞清楚这个问题,我用vue的源码测试了下,下面是vue对数据监测的源码:
Observer

可以看到,当数据是数组时,会停止对数据属性的监测,我们修改一下源码:
修改Observer

使数据为数组时,依然监测其属性,然后在defineReactive函数中的get,set打印一些东西,方便我们知道调用了get以及set。这里加了个简单判断,只看数组元素的get,set
修改defineReactive

然后写了一个简单案例,主要测试使用vm.items[indexOfItem] = newValue改变数组元素能不能被监测到,并响应式的渲染页面
简单案例

运行页面
数组测试

可以看到,运行了6次get,我们数组长度为3,也就是说数组被遍历了两遍。两遍不多,页面渲染一次,可能多次触发一个数据的监听事件,哪怕这个数据只用了一次,具体的需要看尤大代码怎么写的。就拿这个来说,当监听的数据为数组时,会运行dependArray函数(代码在上面图中get的实现里),这个函数里对数组进行了遍历取值操作,所以会多3遍get,这里主要是vue对data中arr数组的监听触发了dependArray函数。

当我们点击其中一个元素的时候,比如我点击的是3
点击3

可以看到会先运行一次set,然后数据更新,重新渲染页面,数组又是被遍历了两遍。

但是!!!数组确实变成响应式的了,也就是说js语法功能并不会限制数组的监测。

这里我们是用长度为3的数组测试的,当我把数组长度增加到9时
新数组测试

可以看到,运行了18次get,数组还是被遍历了两遍,点击某个元素同理,渲染的时候也是被遍历两次。
新数组测试

有了上面的实验,我的结论是数组在vue中是可以实现响应式更新的,但是不明白尤大是出于什么考虑,没有加入这一功能,希望有知道的大佬们不吝赐教


2018-07-27补充

github上提问了尤大
github提问

查看原文

大象无痕 赞了文章 · 2020-07-21

【easy-invoices】electron-vue、sqlite3 项目初探

父母都是做出纳相关的工作,希望我能给他们做个简单的进销存,在上班的时候使用。开发一个不需要花钱买服务器,不需要依赖网络(更新除外),单机版的程序,对于前端出身的我来说,那么electron或nwjs是最好的选择。
electron官网对electron与nwjs的比较
这里我选择了electron,因为很熟悉vue,就使用国人集成的electron-vue进行快速开发。本地数据库采用轻量嵌入型数据库sqlite3,不二之选。UI组件为iview。

物品管理
物品管理

进出明细
进出明细


一、环境准备

安装python2.7 和 Visual Studio 2015


二、安装vue-cli脚手架,初始化electron-vue目录

$ npm install -g vue-cli
$ vue init simulatedgreg/electron-vue easy-invoices

打包选择electron-builder。builder可以打包成具体文件,也可以是exe安装程序,而packager只能打包具体文件。下面会具体说明打包。
该命令会生成一个easy-invoices文件夹,大致目录如下(有细微变动)
目录结构

  • .electron-vue:主要是webpack配置,还有一些封装好了的命令行的输出,供开发、打包调试用。可以自行添加一些配置,如在webpack.render.config.js里添加less-loader和eslint-loader。
  • build:打包需要的素材,例如icon。打包后的默认目录也在于此
  • src:源码,main是主进程部分,render是渲染进程部分,下文会讲到这两个概念。index.ejs会被编译为html的入口。
  • static:静态资源
  • 有一些文件是我后来加上去的,比如eslint相关(.eslintrc.js,.eslintignore),与commit信息校验相关(verify_commmit_msg.js)等
  • .travis.yml为linux构建平台的配置,appveyor.yml为windows构建平台的配置。之后也会详细提到自动化构建。

三、sqlite3集成

nodejs中使用c++模块会涉及到编译问题,该编译常常会导致一些问题发生。
详细的操作请见我的另外一篇文章《electron项目中使用sqlite3的编译问题(windows)》


四、开始开发

在使用electron开发之前,我们需要注意以下几点

  • electron的运行依托于nodejs环境,渲染界面使用chromium。因此,我们开发界面实则编写html,并且在开发的过程中,可以使用nodejs原生模块,比如fs文件模块、os系统模块等。这使得我们的程序有更多的权限和功能,可以非常强大。但在强大的同时,开发者需要担起自身的责任,需要更多的去注意安全问题。
  • 在electron里,最核心的两个概念就是主进程和渲染进程。主进程负责整个程序的调度,控制一些功能,只有一个。而渲染进程负责界面的渲染。他们之间可以相互通信。
  • electron加载html有两种方式,一种通过本地路径读取,一种通过http远程读取。远程读取会有许多限制,防止引起不必要的安全隐患。electron-vue封装好了开发模式和生产模式,开发模式启动webpack-dev-server,渲染进程远程读取,生产模式打包至本地,渲染进程本地路径读取。
  • electron-vue将vue与webpack集成进来快速开发。前端界面使用vue去开发,并使用vue-router做单窗口路由控制。webpack进行模块打包与开发时的监听。electron-vue脚手架提供了vue-electron,并已经封装了这个方法,当运行环境为electron的时候,会将electron挂载在Vue.prototype上。electron对象上有许多api,详情请参考文档。
// vue入口文件
// src/renderer/main.js
if (!process.env.IS_WEB) Vue.use(require('vue-electron'));

...

1.主进程与渲染进程通信

主进程向渲染进程发送消息:

// src/main/index.js
import { BrowserWindow } from 'electron';
const mainWindow = new BrowserWindow();
mainWindow.webContents.send('messageOne', 'haha');

// 某vue组件
<script>
export default {
    created(){
        this.$electron.ipcRenderer.on('messageOne', (event, msg) =>{
            console.log(msg); // 'haha'
        }
    }
}
 <script>

渲染进程向主进程发送消息:

// src/main/index.js
import { ipcMain } from 'electron';
ipcMain.on('messageTwo', (event,msg) => {
    console.log(msg) // 'haha'
});

// 某vue组件
<script>
export default {
    created(){
        this.$electron.ipcRenderer.send('messageTwo', 'haha');
    }
}
 <script>

也可以用once,代表只监听一次。通讯的方法还有多种,比如remote模块等。

2. vue路由

程序刚启动的时候会在根路径下,我们需要进行根路径的路由开发,或者将根路径重定向至开发的路由上。否则会一片白不显示

3. 前端日志

封装一个在开发环境下(环境变量:NODE_ENV=development)打印的函数,在关键的节点进行调用方便调试,比如sql语句等。我仅仅是使用console.log,也有其他的第三方浏览器日志插件可以使用。
本项目里因为没有服务器可上报,所以没有做程序日志的收集,必要时可以去做一些本地日志存储,并且上报,比如错误信息、一些有意义的数据等。

4. sql语句编写

程序启动的时候执行建表的sql并捕获错误,如果表存在会抛出错误,这里我们不用管。暴露出去db对象挂载在Vue.prototype上,即可全局调用,接下来就是在业务中各种拼接编(e)写(xin)sql语句了。
这里我并没有封装数据模型或者使用sequelize等orm库,有兴趣的同学可以尝试。
网上SQL教程与sqlite3教程也比较多,这么不一一描述,下面是代码片段:

// src/renderer/utils/db.js
// 建表脚本,导出db对象供之后使用
import fse from 'fs-extra';
import path from 'path';
import sq3 from 'sqlite3';
import logger from './logger';
import { docDir } from './settings';
// 将数据存至系统用户目录,防止用户误删程序
export const dbPath = path.join(docDir, 'data.sqlite3');
fse.ensureFileSync(dbPath);

const sqlite3 = sq3.verbose();
const db = new sqlite3.Database(dbPath);
db.serialize(() => {
  /**
   * 物品表 GOODS
   * name 品名
   * standard_buy_unit_price 标准进价
   * standard_sell_unit_price 标准售价
   * total_amount 总金额
   * total_count 总数量
   * remark 备注
   * create_time 创建时间
   * update_time 修改时间
   */
  db.run(`CREATE TABLE GOODS(
    id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
    name VARCHAR(255) NOT NULL,
    standard_buy_unit_price DECIMAL(15,2) NOT NULL,
    standard_sell_unit_price DECIMAL(15,2) NOT NULL,
    total_amount DECIMAL(15,2) NOT NULL,
    total_count DECIMAL(15,3) NOT NULL,
    remark VARCHAR(255) NOT NULL,
    create_time INTEGER NOT NULL,
    update_time INTEGER NOT NULL
    )`, err => {
    logger(err);
  });

  /**
   * 进出明细表 GOODS_DETAIL_LIST
   * goods_id 物品id
   * count 计数(+加 -减)
   * actual_buy_unit_price 实际进价
   * actual_sell_unit_price 实际售价
   * amount 实际金额
   * remark 备注
   * latest 是否某物品最新一条记录(不是最新操作无法删除)(1是 0不是)
   * create_time 创建时间
   * update_time 修改时间
   */
  db.run(`CREATE TABLE GOODS_DETAIL_LIST(
    id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
    goods_id INTEGER NOT NULL, 
    count DECIMAL(15,3) NOT NULL,
    actual_sell_unit_price DECIMAL(15,2) NOT NULL,
    actual_buy_unit_price DECIMAL(15,2) NOT NULL,
    amount DECIMAL(15,2) NOT NULL,
    remark VARCHAR(255) NOT NULL,
    latest INTEGER NOT NULL,
    create_time INTEGER NOT NULL,
    update_time INTEGER NOT NULL,
    FOREIGN KEY (goods_id) REFERENCES GOODS(id)
    )`, err => {
    logger(err);
  });
});

export default db;

5. 数据文件及用户配置、

考虑到用户手误卸载或者删除程序安装目录,将数据文件和用户配置存放在C:Users&dollar;{username}easy-invoices路径下。这样如果不小心删了,重新安装还是可以和之前一样。做得更好一些可以在卸载的时候询问是否删除数据和配置(还没尝试过,不知道electron-builder是否支持)

6. 升级方案

不同于B/S架构,C/S架构必须要做好自己的升级方案,否则用户装好了程序就无法再进行更新了。
主进程使用electron-updater来控制自动更新,渲染进程来做更新的逻辑,每个程序更新的流程都不一样,我的程序是每次启动检测更新,如果有更新就自动下载,下载完成后提示用户是否需要重启更新,用户选择取消则每次开启的时候都会提示一下,用户选择升级那么就重启升级。
因为我的程序是托管在github上,所以不需要设置feedurl(feedurl有默认值,和打包设置有关,我的项目中默认会去github的release api上检测)。如果放在其他服务器上,需要编写检测接口并设置url。electron-updater官方文档

下面是代码片段

$ npm i electron-updater

主进程中

// src/main/index.js
import { autoUpdater } from 'electron-updater';

app.on('ready', () => {
  if (process.env.NODE_ENV === 'production') autoUpdater.checkForUpdatesAndNotify();
});

function sendUpdateMessage(message, data) {
  //往渲染进程发送消息,mainWindow来自new BrowserWindow
  mainWindow.webContents.send('update-message', { message, data });
}

// 阻止程序关闭自动安装升级
autoUpdater.autoInstallOnAppQuit = false;

autoUpdater.on('error', data => {
  sendUpdateMessage('error', data);
});

/* // 检查更新
autoUpdater.on('checking-for-update', data => {
  sendUpdateMessage('checking-for-update', data);
});*/

// 有可用更新
autoUpdater.on('update-available', data => {
  sendUpdateMessage('update-available', data);
});

// 已经最新
autoUpdater.on('update-not-available', data => {
  sendUpdateMessage('update-not-available', data);
});

// 更新下载进度事件
autoUpdater.on('download-progress', data => {
  sendUpdateMessage('download-progress', data);
});

// 更新下载完成事件
autoUpdater.on('update-downloaded', () => {
  sendUpdateMessage('update-downloaded', {});
  ipcMain.once('update-now', () => {
    autoUpdater.quitAndInstall();
  });
});

注意:在升级中可能会有改表结构的操作,我在settings.json里存有版本信息,启动的时候将程序的版本号与settings里面的版本号对比,进行升级,升级完成之后将settings里的版本设置为程序版本

// src/renderer/utils/upgrade.js
import settings from './settings';
import packageJson from '../../../package.json';
// 程序当前版本
const appCurrentVersion = packageJson.version;

import db from './db';

// 罗列增量升级脚本
const incrementalUpgrade = {
  '1.0.1':()=>{
    db.run(
    //修改表数据、结构的脚本等
    );
  },
  '1.0.2':()=>{
    db.run(
    //修改表数据、结构的脚本等
    );
  },
}

// 升级前版本
const beforeUpgradeVersion = settings.get('version');
// 用户可能有很多个版本没有升级,寻找执行的脚本 增量执行。
// 遍历incrementalUpgrade对象,大于beforeUpgradeVersion的脚本都要依次执行。(比较时可以把点去掉转为数字类型比较)
...

// 脚本执行完毕
settings.set('version', appCurrentVersion);

下载前可以拿到更新日志、时间、版本号和包大小,下载时可以拿到速度。部分效果展示:
20180813221631144.png
20180813221821255.png

7. 打包

前文提到,我采用的是electron-builder进行打包。electron-builder官方文档
打包的主要配置在package.json里:

{
    "scripts":{
        "build": "node .electron-vue/build.js && electron-builder",
        "build:dir": "node .electron-vue/build.js && electron-builder --dir"
    },
    "build": {
        "productName": "easy-invoices",
        "copyright": "caandoll",
        "appId": "org.caandoll.easy-invoices",
        "directories": {
          "output": "build"
        },
        "files": [
          "dist/electron/**/*"
        ],
        "dmg": {
          "contents": [
            {
              "x": 410,
              "y": 150,
              "type": "link",
              "path": "/Applications"
            },
            {
              "x": 130,
              "y": 150,
              "type": "file"
            }
          ]
        },
        "mac": {
          "icon": "build/icons/icon.png"
        },
        "win": {
          "icon": "build/icons/icon.png"
        },
        "linux": {
          "icon": "build/icons/icon.png"
        },
        "nsis": {
          "oneClick": false,
          "allowToChangeInstallationDirectory": true
        }
    }
}

scripts:

  • build:打包成exe安装程序
  • build:dir:打包成文件形式

build:

  • productName:项目名
  • copyright:版权
  • directories:打包目录
  • win: windows配置。icon为程序图标目录,windows图标至少需要320 x 320,否则报错
  • nsis:windows安装程序exe配置,如果不配置,那么一键安装至C盘User一个local app目录下,不符合程序使用要求,这里我设置了oneClick:false和allowToChangeInstallationDirectory:true,就是不让程序一键安装,让用户去选择安装目录。
  • 其他如appId,dmg,linux、mac都是macOS和linux系统配置,没有仔细研究

8. CI自动构建发布

travis和appveyor是开源的两个自动化构建平台,免费服务于github等开源项目(不开源项目貌似要给钱)。如果你是在其他这两个CI平台不支持的仓库,可使用其他构建工具,原理相同。

①. 在https://github.com/settings/tokens Generate new token,写上描述,勾上发布权限,生成token。该token只可见一次,注意保存

20180809161035898.png

②. https://www.appveyor.com/注册用户,使用github登录。然后开启该项目的构建。

2018080916190511.png
20180809162000320.png

③. 将第一步生成的token填至项目环境变量,参数名为GH_TOKEN。发布的时候会自动使用GH_TOKEN进行github release api的调用。

20180809162324607.png

④. package.json
{
    "repository": {
        "type": "git",
        "url": "https://github.com/CaanDoll/easy-invoices.git"
    },
    "scripts":{
        "build:ci": "node .electron-vue/build.js && electron-builder --publish always"
    },
}
  • build:ci:执行后,不仅打包,还会将打包后程序上传,发布成github的release草稿,手动编辑后即可发布。
⑥. appveyor.yml
version: 0.0.{build}

branches:
  only:
    - master

image: Visual Studio 2017
platform:
  - x64

cache:
  - node_modules
  - '%APPDATA%\npm-cache'
  - '%USERPROFILE%\.electron'
  - '%USERPROFILE%\AppData\Local\Yarn\cache'

init:
  - git config --global core.autocrlf input

install:
  - ps: Install-Product node 8 x64
  - yarn

build_script:
  - yarn build:ci

test: off
  • version:为构建的版本号,会自增,这个和程序的版本号没有关系
  • branches:指定在哪个分支进行构建
  • image:基础镜像,windows程序构建会用到vs
  • platform:系统位数:如x86(32位),x64(64位)
  • cache:npm缓存目录
  • init:初始执行命令,将所有代码换行符改为CRLF模式
  • install:安装包
  • build_script:执行命令

接下来提交在github master分支或者merge到master分支(申请merge之后也会触发)就可以触发构建了,在appveyor平台上可以看到。


五、其他一些细节

1.打开系统默认浏览器对应链接或者打开我的电脑对应文件目录

如果使用一般的a标签,会直接将程序的界面跳转至这个链接,因为本身就是浏览器内核。加上target:_blank的话更会没有反应了。这个时候需要调用electron.shell。上面的openExternal(url)方法就是打开浏览器,openItem(path)打开文件目录。

// vue入口文件
// src/renderer/main.js
if (!process.env.IS_WEB) Vue.use(require('vue-electron'));

// 某页面组件xxx.vue
<script>
export default {
  methods: {
    openUrl(url) {
      this.$electron.shell.openExternal(url);
    },
    openPath(path) {
      this.$electron.shell.openItem(path);
    },
  }
};
</script>

2.导出excel(下载文件)

如果在服务端进行导出,有两个步骤,第一步是将数据填充并生成excel,第二步是将文件发送出去。使用electron本地进行导出也不例外,但因为不是调用http接口,会有一些差异。
nodejs生成excel在这里就不多描述,以后我会补充相应的文章。在这里先推荐这两个库,如果生成的excel比较简单,横行数列并没有任何样式的,可以使用node-xlsx。如果需要生成较为复杂的excel,比如有样式要求,有合并单元格的需求,可以使用ejsExcel
假设我们已经导出了一个名为test.xlsx的excel在系统临时目录(os.tmpdir()):C:UsersusernameAppDataLocalTempappnametest.xlsx

// src/main/index.js
import { ipcMain } from 'electron';
// mainWindow来自new BrowserWindow
ipcMain.on('download', (event, downloadPath) => {
  mainWindow.webContents.downloadURL(downloadPath);// 这个时候会弹出让用户选择下载目录
  mainWindow.webContents.session.once('will-download', (event, item) => {
    item.once('done', (event, state) => {
      // 成功的话 state为completed 取消的话 state为cancelled
      mainWindow.webContents.send('downstate', state);
    });
  });
});

// 渲染进程
ipcRenderer.send('download', 'C:\Users\username\AppData\Local\Temp\appname\test.xlsx');
ipcRenderer.once('downstate', (event, arg) => {
  if (arg === 'completed') {
    console.log('下载成功');
  } else if (arg === 'cancelled'){
    console.log('下载取消');
  } else {
    console.log('下载失败')
  }

3.窗口相关

① 窗口栏

原生的窗口栏不是那么美观,我们可以去掉原生窗口栏,自己写一个。
主进程

// src/main/index.js
import { BrowserWindow、ipcMain } from 'electron';
// 创建窗口时配置
const mainWindow = new BrowserWindow({
    frame: false, // 去掉原生窗口栏
    ...
});

// 主进程监听事件进行窗口最小化、最大化、关闭  
// 窗口最小化
ipcMain.on('min-window', () => {
  mainWindow.minimize();
});
// 窗口最大化
ipcMain.on('max-window', () => {
  if (mainWindow.isMaximized()) {
    mainWindow.restore();
  } else {
    mainWindow.maximize();
  }
});
// 关闭
ipcMain.on('close-window', () => {
  mainWindow.close();
});

头部组件或其他组件,这样就可以在自己定义的元素上去执行窗口操作了

<script>
export default {
  methods: {
    minWindows() {
      this.$electron.ipcRenderer.send('min-window');
    },
    maxWindows() {
      this.$electron.ipcRenderer.send('max-window');
    },
    closeWindows() {
      this.$electron.ipcRenderer.send('close-window');
    },
};
</script>

css设置拖拽区域,拖拽区域会自动有双击最大化的功能,注意:拖拽区域内的点击、移入移出等事件将无效,需要将拖拽区域内的按钮等元素设为非拖拽区域即可

    header {
        -webkit-app-region: drag; // 拖拽区域
        .version {
            .ivu-tooltip {
                -webkit-app-region: no-drag; // 非拖拽区域
            }
        }
        .right {
            a {
                -webkit-app-region: no-drag; // 非拖拽区域
            }
        }
    }
② 启动时窗口白屏

程序启动时,界面渲染需要一定时间,导致白屏一下,体验不好。解决方案一种是将程序的背景色设为html的背景色,另外一种就是等界面加载完毕之后再显示窗口,代码如下:
主进程中

// src/main/index.js
import { BrowserWindow} from 'electron';
const mainWindow = new BrowserWindow({
    show: false,
    ...
 });
// 加载好html再呈现window,避免白屏
mainWindow.on('ready-to-show', () => {
    mainWindow.show();
    mainWindow.focus();
});

结语

electron非常好玩,它解放了我们在浏览器中开发界面的束缚。C/S架构也有很多不同于功能点需要多多考虑。第一次写比较长的文章,个中可能会有手误或者知识错误,顺序也不是最理想的。欢迎讨论,也请各路大牛多多指教,指出不正!

查看原文

赞 7 收藏 3 评论 2

大象无痕 关注了用户 · 2020-06-23

saucxs @saucxs

昵称:saucxs | songEagle | 松宝写代码

github:https://github.com/saucxs

时间永远是自己,每分每秒也都是为将来铺垫和增值。

一、概览
【模块】:研发知识体系构建,精选文章,技术分享,项目实战,实验室,每日一题,进阶学习,字节内推。

二、每日一题
关注后,「周一到周五」每天推送每日一题。

三、字节内推
回复「校招」
回复「社招」
回复「实习生」

关注 13

大象无痕 赞了文章 · 2020-04-10

react-router-dom@5.x官方文档翻译

简介

这是我学习react-router-dom@5.1.2时,为了加深自己对react-router-dom的理解和帮助一些英文不好的同学,对官方文档进行了翻译,本人水平有限,如有理解和翻译错误,欢迎大家指正。官网地址

快速入门

要在web应用中开始使用React Router,您将需要一个React Web应用程序.如果您需要创建一个,我们建议您尝试Create React App。这是一个非常流行的工具,可与React Router很好地配合使用。

首先,安装create-react-app并使用它创建一个新项目。

安装

您可以使用npm或yarn从公共npm注册表中安装React Router。由于我们构建的是web app,因此在本指南中将使用react-router-dom。

npm install -g create-react-app       // 全局安装 create-react-app
create-react-app demo-app             // 创建一个react项目
cd demo-app                           // 进入react项目目录
npm install react-router-dom          // 安装react-router-dom

接下来,将以下两个示例之一复制/粘贴到src/App.js中。

第一个示例:基本路由

在此示例中,路由器处理了3个“页面”:主页、关于页面和用户页面。当您点击不同的<Link>时,这个路由会渲染匹配的<Route>。

注意:其实<Link>最后渲染出来是一个有真实href的标签,因此使用键盘导航或屏幕阅读器的人也可以使用react-router-dom。

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter, Switch, Route, Link } from 'react-router-dom';

function Home(props) {
    console.log('Home=>', props);
    return <h2>Home</h2>
}

function About(props) {
    console.log('About=>', props);
    return <h2>About</h2>;
}

function Users(props) {
    console.log('Users=>', props);
    return <h2>Users</h2>;
}

function App() {
    return <BrowserRouter>
        <div>
            <nav>
                <ul>
                    <li>
                        <Link to={'/'}>Home</Link>
                    </li>
                    <li>
                        <Link to={'/about'}>About</Link>
                    </li>
                    <li>
                        <Link to={'/users'}>Users</Link>
                    </li>
                </ul>
            </nav>
            {/* <Switch>通过查找所有的子<Route>并渲染与当前URL匹配的第一个<Route>的内容 */}
            <Switch>
                <Route path={'/about'}>
                    <About />
                </Route>
                <Route path={'/users'} children={<Users />}/>
                <Route path={'/'}>
                    <Home />
                </Route>
            </Switch>
        </div>
    </BrowserRouter>
}

ReactDOM.render(<App />, document.querySelector('#root'));

第二个示例:嵌套路由

此示例显示了嵌套路由的工作方式。路线/topics会加载Topics组件,在这个组件上的path:id值上有条件地渲染任何其他<Route>。

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter, Switch, Route, Link, useRouteMatch, useParams } from 'react-router-dom';

function Home(props) {
    console.log('Home=>', props);
    return <h2>Home</h2>
}

function About(props) {
    console.log('About=>', props);
    return <h2>About</h2>;
}

function Topic() {
    let { topicId } = useParams();
    return <h3>Requested topic ID: {topicId}</h3>
}

function Topics() {
    const match = useRouteMatch();
    console.log('match=>', match);
    return (
        <div>
            <h2>Topics</h2>
            <ul>
                <li>
                    <Link to={`${match.url}/components`}>Components</Link>
                </li>
                <li>
                    <Link to={`${match.url}/props-v-state`}>Props v. State</Link>
                </li>
            </ul>
            
            {/*
                Topics页面有自己的<Switch>,其中包含更多的路线,建立在/topics路径之上
                您可以将第二个<Route>视为所有主题的“索引”页面,或者当未选择任何主题时显示的页面
            */}
            <Switch>
                <Route path={`${match.path}/:topicId`}>
                    <Topic />
                </Route>
                <Route path={match.path}>
                    <h3>Please select a topic.</h3>
                </Route>
            </Switch>
        </div>
    );
}

function App() {
    return <BrowserRouter>
        <ul>
            <li>
                <Link to={'/'}>Home</Link>
            </li>
            <li>
                <Link to={'/about'}>About</Link>
            </li>
            <li>
                <Link to={'/topics'}>Topics</Link>
            </li>
        </ul>
        
        <Switch>
            <Route path={'/about'}>
                <About />
            </Route>
            <Route path={'/topics'}>
                <Topics />
            </Route>
            <Route path={'/'}>
                <Home />
            </Route>
        </Switch>
    </BrowserRouter>
}

ReactDOM.render(<App />, document.querySelector('#root'));

继续

希望这些示例能让您对使用React Router创建web app有点感觉。继续阅读可以了解有关React Router中主要组件的更多信息!

主要组件

React Router中的组件主要分为三类:

  • 路由器,例如<BrowserRouter>和<HashRouter>
  • 路由匹配器,例如<Route>和<Switch>
  • 导航,例如<Link>,<NavLink>和<Redirect>

在Web应用程序中使用的所有组件都应从react-router-dom导入。

路由器

每个React Router应用程序的核心应该是路由器组件。对于Web项目,react-router-dom提供<BrowserRouter>和<HashRouter>路由器。两者之间的主要区别在于它们存储URL和与Web服务器通信的方式。

  1. <BrowserRouter>使用常规URL路径。 这些通常是外观最好的网址,但它们要求您的服务器配置正确。 具体来说,您的Web服务器需要在所有由React Router客户端管理的URL上提供相同的页面。Create React App在开发中即开即用地支持此功能,并附带有关如何配置生产服务器的说明
  2. <HashRouter>将当前位置存储在URL的哈希部分中,因此URL看起来类似于http://example.com/#/your/page。 由于哈希从不发送到服务器,因此这意味着不需要特殊的服务器配置。

要使用路由器,只需确保将其渲染在元素层次结构的根目录下即可。 通常,您会将顶级<App>元素包装在路由器中,如下所示:

import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter } from "react-router-dom";

function App() {
  return <h1>Hello React Router</h1>;
}

ReactDOM.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>,
  document.getElementById("root")
);

路线匹配器

有两个路线匹配组件:Switch和Route。渲染<Switch>时,它会搜索其子元素<Route>,以查找其路径与当前URL匹配的元素。当找到一个时,它将渲染该<Route>并忽略所有其他路由。这意味着您应该将<Route>包含更多特定路径(通常较长)的路径放在不那么特定路径之前。

如果没有<Route>匹配,则<Switch>不渲染任何内容(null)。

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter, Switch, Route } from 'react-router-dom';

function App() {
    return <div>
        <Switch>
            {/* 如果当前URL是/ about,那么将渲染此路由,而其余路由将被忽略 */}
            <Route path={'/about'}>
                <h2>About</h2>
            </Route>
            {/* 请注意这两个路由的顺序。 更具体的path="/contact/id"位于path="/contact"之前,因此在查看单个联系人时将显示这个<Route> */}
            <Route path={'/contact/:id'}>
                <h2>Contact</h2>
            </Route>
            <Route path={'/contact'}>
                <h2>AllContact</h2>
            </Route>
            {/*
                如果先前的路由均未呈现任何内容,则此路由将充当后备路径。
                重要提示:路径="/"的路线将始终匹配任何路径的URL,因为所有URL均以/开头。 所以这就是为什么我们把这这个<Route>放在最后
            */}
            <Route path={'/'}>
                <h2>Home</h2>
            </Route>
        </Switch>
    </div>
}

ReactDOM.render(<BrowserRouter>
    <App />
</BrowserRouter>, document.querySelector('#root'));

⚠️ 需要注意的重要一件事是<Route path>匹配URL的开头,而不是整个开头。所以,<Route path ="/">将始终与任意一个URL匹配。因此,我们通常将此<Route>放在<Switch>的最后。另一种可能的解决方案是使用确实与整个URL匹配的<Route exact path="">。exact属性表示精准匹配。

⚠️注意:尽管React Router确实支持在<Switch>之外渲染<Route>元素,从5.1版开始,我们建议您改用useRouteMatch钩子。此外,我们不建议您渲染不带路径的<Route>,而是建议您使用钩子来访问您所使用的任何变量。

导航(或路线更改器)

React Router提供了一个<Link>组件来在您的应用程序中创建链接。 无论在何处渲染<Link>,锚点都将渲染在HTML文档中。

<NavLink>是<Link>的一种特殊类型,当其prop与当前位置匹配时,可以将其自身设置为“active”。

任何时候要强制导航,都可以渲染<Redirect>。渲染<Redirect>时,它将会使用其props进行导航

<Link to="/">Home</Link>
// <a href="/">Home</a>

<NavLink to="/react" activeClassName="hurray">
  React
</NavLink>
// 当URL为/react的时候, 渲染出来的以下内容:
// <a href="/react" className="hurray">React</a>
// 如果是其他URL,则渲染为:
// <a href="/react">React</a>

// 重定向到/login
<Redirect to="/login" />

NavLink例子:

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter, Switch, Route, NavLink } from 'react-router-dom';

function NavigationApp() {
    return <BrowserRouter>
        <ul>
            <li>
                <NavLink to={'/react'}>React</NavLink>
            </li>
            <li>
                <NavLink to={'/redux'}>redux</NavLink>
            </li>
        </ul>
        <div>
            <Switch>
                <Route path={'/react'}>
                    <h1>React</h1>
                </Route>
                <Route path={'/redux'}>
                    <h1>Redux</h1>
                </Route>
            </Switch>
        </div>
    </BrowserRouter>
}

ReactDOM.render(<NavigationApp />, document.querySelector('#root'));

服务器渲染

代码分割

网络应用的一个重要特色就是:我们无需让访问者下载整个应用程序即可使用,您可以将代码拆分视为增量下载应用程序。为此,我们将使用webpack,@babel/plugin-syntax-dynamic-import,和loadable-components做代码分割。

webpack内置了对动态导入的支持; 但是,如果您使用的是Babel(例如,将JSX编译为JavaScript),则需要使用@babel/plugin-syntax-dynamic-import插件。这是仅语法的插件,这意味着Babel不会进行任何其他转换。该插件仅允许Babel解析动态导入,因此webpack可以将它们捆绑为代码拆分。 您的.babelrc应该使用如下配置:

{
  "presets": ["@babel/preset-react"],
  "plugins": ["@babel/plugin-syntax-dynamic-import"]
}

loadable-components是用于通过动态导入加载组件的库。 它自动处理各种边缘情况,并使代码拆分变得简单! 这是有关如何使用loadable-components的示例:

import loadable from "@loadable/component";
import Loading from "./Loading.js";

const LoadableComponent = loadable(() => import("./Dashboard.js"), {
  fallback: <Loading />
});

export default class LoadableDashboard extends React.Component {
  render() {
    return <LoadableComponent />;
  }
}

这一切就是这么简单! 只需使用LoadableDashboard(或任何您命名的组件),当您在应用程序中使用它时,它将自动加载并渲染。fallback是一个占位符组件,用于在加载实际组件时显示。
完整的文档在这里

代码拆分和服务器端渲染

loadable-components包含服务器端渲染的指南

滚动还原

在早期版本的React Router中,我们提供了对滚动恢复的开箱即用的支持,从那以后人们一直在要求它。 希望本文档可以帮助您从滚动条和路由中获得所需的信息!
浏览器开始以自己的history.pushState处理滚动还原,其处理方式与使用普通浏览器导航时的处理方式相同。它已经可以在Chrome浏览器中使用,而且非常棒,这是滚动恢复规范
由于浏览器开始处理“默认情况”,并且应用具有不同的滚动需求(例如本网站!),因此我们不提供默认滚动管理功能。 本指南应帮助您实现任何滚动需求。

滚动到顶部

在大多数情况下,您所需要做的只是“滚动到顶部”,因为您有一个较长的内容页面,该页面在导航到该页面时始终保持向下滚动。 使用<ScrollToTop>组件可以轻松处理此问题,该组件将在每次导航时向上滚动窗口:
创建滚动到顶部组件:

import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
export default function ScrollToTop() {
    const { pathname } = useLocation();
    console.log('pathname=>', pathname);
    useEffect(() => {
        window.scrollTo(0, 0);
    }, [ pathname ]);
    return null;
}

如果您尚未运行React 16.8,则可以使用React.Component子类执行相同的操作:

import React from "react";
import { withRouter } from "react-router-dom";

class ScrollToTop extends React.Component {
  componentDidUpdate(prevProps) {
    if (
      this.props.location.pathname !== prevProps.location.pathname
    ) {
      window.scrollTo(0, 0);
    }
  }

  render() {
    return null;
  }
}

export default withRouter(ScrollToTop);

然后在您的应用程序的顶部渲染它,但是要把它路由器下面:

import React from 'react'
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import ScrollToTop from './ScrollToTop'

function App() {
    return <BrowserRouter>
        <ScrollToTop/>
        <h1>App</h1>
    </BrowserRouter>
}

ReactDOM.render(<App />, document.querySelector('#root'));

如果您将标签页接口连接到路由器,那么当他们切换标签页时,您可能不想滚动到顶部。 那么,您需要在的特定位置使用<ScrollToTopOnMount>?

import { useEffect } from "react";

function ScrollToTopOnMount() {
  useEffect(() => {
    window.scrollTo(0, 0);
  }, []);

  return null;
}

// 使用以下代码将此内容渲染到某处:
// <Route path="..." children={<LongContent />} />
function LongContent() {
  return (
    <div>
      <ScrollToTopOnMount />

      <h1>Here is my long content page</h1>
      <p>...</p>
    </div>
  );
}

再说一次,如果您运行的React小于16.8,则可以对React.Component子类做同样的事情:

with a React.Component subclass:import React from "react";

class ScrollToTopOnMount extends React.Component {
  componentDidMount() {
    window.scrollTo(0, 0);
  }

  render() {
    return null;
  }
}

// 使用以下代码将此内容渲染到某处:
// <Route path="..." children={<LongContent />} />
class LongContent extends React.Component {
  render() {
    return (
      <div>
        <ScrollToTopOnMount />

        <h1>Here is my long content page</h1>
        <p>...</p>
      </div>
    );
  }
}

通用解决方案

对于通用解决方案(以及哪些浏览器已开始在本机实现),我们谈论的是两件事:
1、向上滚动导航,这样就不会启动滚动到底部的新屏幕
2、恢复窗口的滚动位置和“后退”和“前进”单击上的溢出元素(但不单击“链接”单击!)
在某一时刻,我们希望提供一个通用的API。 这就是我们要研究的方向:

<Router>
  <ScrollRestoration>
    <div>
      <h1>App</h1>

      <RestoredScroll id="bunny">
        <div style={{ height: "200px", overflow: "auto" }}>
          I will overflow
        </div>
      </RestoredScroll>
    </div>
  </ScrollRestoration>
</Router>

首先,ScrollRestoration在导航中向上滚动窗口。其次,它将使用location.key将窗口滚动位置和RestoredScroll组件的滚动位置保存到sessionStorage。然后,在安装ScrollRestoration或RestoredScroll组件时,它们可以从sessionStorage查找其位置。

最棘手的部分是定义一个"opt-out"的API,当你不想滚动窗口时进行管理。例如,如果您在页面内容中浮动了一些选项卡导航,则可能不想滚动到顶部(选项卡可能会滚动到视图之外!)。当我们得知Chrome现在可以为我们管理滚动位置,并意识到不同的应用程序将具有不同的滚动需求时,我们有点迷失了我们需要提供某些东西的信念,尤其是当人们只想滚动到顶部时( 您可以直接将其直接添加到您的应用中)。基于此,我们不再有足够的力气自己完成工作(就像您一样,我们的时间有限!)。 但是,我们很乐意为有志于实施通用解决方案的任何人提供帮助。 一个可靠的解决方案甚至可以存在于项目中。 如果您开始使用它,请与我们联系:)

设计原理

本指南的目的是说明使用React Router时要具有的思维模型。 我们称之为“动态路由”,它与您可能更熟悉的“静态路由”完全不同。

静态路由

如果您使用过Rails,Express,Ember,Angular等,则使用了静态路由。 在这些框架中,您需要在进行任何渲染之前将路由声明为应用初始化的一部分。 React Router pre-v4也是静态的(大部分是静态的)。让我们看一下在express中如何配置路由:

Express路由配置模式:
app.get("/", handleIndex);
app.get("/invoices", handleInvoices);
app.get("/invoices/:id", handleInvoice);
app.get("/invoices/:id/edit", handleInvoiceEdit);

app.listen();

请注意在app监听之前如何声明路由。 我们使用的客户端路由器相似。 在Angular中,您先声明routes,然后在渲染之前将其导入顶级的AppModule中:

// Angular的路由配置样式:
const appRoutes: Routes = [
  {
    path: "crisis-center",
    component: CrisisListComponent
  },
  {
    path: "hero/:id",
    component: HeroDetailComponent
  },
  {
    path: "heroes",
    component: HeroListComponent,
    data: { title: "Heroes List" }
  },
  {
    path: "",
    redirectTo: "/heroes",
    pathMatch: "full"
  },
  {
    path: "**",
    component: PageNotFoundComponent
  }
];

@NgModule({
  imports: [RouterModule.forRoot(appRoutes)]
})
export class AppModule {}

Ember具有常规的route.js文件,该版本会为您读取并导入到应用程序中。 同样,这是在您的应用渲染之前发生的。

// Ember 路由配置样式:
Router.map(function() {
  this.route("about");
  this.route("contact");
  this.route("rentals", function() {
    this.route("show", { path: "/:rental_id" });
  });
});

export default Router;

虽然API是不同的,他们都有着“静态路由”的模式。 React Router也跟进了直到v4。
为了成功使用React Router,您需要忘记所有这些!

背后故事

坦率地说,我们对v2采取React Router的方向感到非常沮丧。 我们(Michael和Ryan)感到受到API的限制,认识到我们正在重新实现React的各个部分(生命周期等),而这与React为构建UI提供的思维模型不符。

我们走在一家酒店的走廊上,正在讨论如何解决这个问题。我们互相问:“如果我们使用我们在工作室里教的模式来建造路由器,那会是什么样子?”

仅仅在开发的几个小时内,我们就有了一个概念证明,我们知道这就是我们想要的路由的未来。我们最终得到的API不是React的“外部”API,而是一个由React的其余部分组成的API,或者自然地与之匹配。我们想你会喜欢的。

动态路由

当说动态路由时,是指在您的应用渲染时发生的路由,而不是在运行的应用之外的配置或约定中进行。 这意味着几乎所有内容都是React Router中的一个组件。 这是对该API的60秒回顾,以了解其工作原理:

首先,为您要定位的环境获取一个Router组件,并将其呈现在应用程序的顶部。

// react-native
import { NativeRouter } from "react-router-native";

// react-dom (我们将在这里使用什么)
import { BrowserRouter } from "react-router-dom";

ReactDOM.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>,
  el
);

接下来,获取链接组件以链接到新位置:

const App = () => (
  <div>
    <nav>
      <Link to="/dashboard">Dashboard</Link>
    </nav>
  </div>
);

最后,渲染一个Route在用户访问/dashboard时显示一些UI。

const App = () => (
  <div>
    <nav>
      <Link to="/dashboard">Dashboard</Link>
    </nav>
    <div>
      <Route path="/dashboard" component={Dashboard} />
    </div>
  </div>
);

Route将渲染<Dashboard {... props} />,其中props是路由器特定的东西,props对象以这三个关键对象{match,location, history}。 如果用户不在/dashboard上,则Route将渲染null。 差不多就够了。

嵌套路由

很多路由器都有“嵌套路由”的概念。如果您使用的是v4之前的React Router版本,那么您也会知道它是这么做的!当您从静态路由配置转移到动态渲染路由时,如何“嵌套路由”?如何嵌套div呢?

const App = () => (
  <BrowserRouter>
    {/* 这是一个 div */}
    <div>
      {/* 这是一个 Route */}
      <Route path="/tacos" component={Tacos} />
    </div>
  </BrowserRouter>
);

// 当网址与`/ tacos`相匹配时,渲染此组件
const Tacos = ({ match }) => (
  // 这是一个嵌套的div
  <div>
    {/* 这是一条嵌套路线match.url帮助我们建立相对路径 */}
    <Route path={match.url + "/carnitas"} component={Carnitas} />
  </div>
);

看到路由器没有“嵌套”API了吗?路由只是一个组件,就像div一样。要嵌套一个路由或div,你只需要...
让我们更加棘手。

响应式路由

考虑用户导航到/invoices。 您的应用程序适应不同的屏幕尺寸,它们的viewport狭窄,因此您只向他们显示发票清单和发票仪表板的链接。 他们可以从那里更深入地导航。

小屏幕
url: /invoices

+----------------------+
|                      |
|      Dashboard       |
|                      |
+----------------------+
|                      |
|      Invoice 01      |
|                      |
+----------------------+
|                      |
|      Invoice 02      |
|                      |
+----------------------+
|                      |
|      Invoice 03      |
|                      |
+----------------------+
|                      |
|      Invoice 04      |
|                      |
+----------------------+

在较大的屏幕上,我们想显示一个主从视图,其中导航在左侧,仪表板或特定发票在右侧。

大屏幕
url: /invoices/dashboard

+----------------------+---------------------------+
|                      |                           |
|      Dashboard       |                           |
|                      |   Unpaid:             5   |
+----------------------+                           |
|                      |   Balance:   $53,543.00   |
|      Invoice 01      |                           |
|                      |   Past Due:           2   |
+----------------------+                           |
|                      |                           |
|      Invoice 02      |                           |
|                      |   +-------------------+   |
+----------------------+   |                   |   |
|                      |   |  +    +     +     |   |
|      Invoice 03      |   |  | +  |     |     |   |
|                      |   |  | |  |  +  |  +  |   |
+----------------------+   |  | |  |  |  |  |  |   |
|                      |   +--+-+--+--+--+--+--+   |
|      Invoice 04      |                           |
|                      |                           |
+----------------------+---------------------------+

现在暂停一分钟,并考虑两种屏幕尺寸的/invoices网址。 它甚至是大屏幕的有效路线吗? 我们应该在右边放什么?

大屏幕
url: /invoices
+----------------------+---------------------------+
|                      |                           |
|      Dashboard       |                           |
|                      |                           |
+----------------------+                           |
|                      |                           |
|      Invoice 01      |                           |
|                      |                           |
+----------------------+                           |
|                      |                           |
|      Invoice 02      |             ???           |
|                      |                           |
+----------------------+                           |
|                      |                           |
|      Invoice 03      |                           |
|                      |                           |
+----------------------+                           |
|                      |                           |
|      Invoice 04      |                           |
|                      |                           |
+----------------------+---------------------------+

在大屏幕上,/invoices不是有效的路径,但在小屏幕上则是! 为了使事情变得更有趣,请考虑使用大型手机的人。 他们可能会纵向查看/invoices,然后将手机旋转至横向。 突然,我们有足够的空间来显示主从界面,因此您应该立即进行重定向!
React Router以前版本的静态路由并没有真正解决这个问题的方法。 但是,当路由是动态的时,您可以声明性地组合此功能。 如果您开始考虑将路由选择为UI,而不是静态配置,那么您的直觉将引导您进入以下代码:

const App = () => (
  <AppLayout>
    <Route path="/invoices" component={Invoices} />
  </AppLayout>
);

const Invoices = () => (
  <Layout>
    {/* 总是显示导航 */}
    <InvoicesNav />

    <Media query={PRETTY_SMALL}>
      {screenIsSmall =>
        screenIsSmall ? (
          // 小屏幕没有重定向
          <Switch>
            <Route
              exact
              path="/invoices/dashboard"
              component={Dashboard}
            />
            <Route path="/invoices/:id" component={Invoice} />
          </Switch>
        ) : (
          // 大屏幕呢!
          <Switch>
            <Route
              exact
              path="/invoices/dashboard"
              component={Dashboard}
            />
            <Route path="/invoices/:id" component={Invoice} />
            <Redirect from="/invoices" to="/invoices/dashboard" />
          </Switch>
        )
      }
    </Media>
  </Layout>
);

当用户将手机从纵向旋转到横向时,此代码将自动将其重定向到仪表板。 有效routes会根据用户手中移动设备的动态性质而变化。
这只是一个例子。 我们可以讨论许多其他内容,但我们将总结以下建议:为了使您的直觉与React Router的直觉相符,请考虑组件而不是静态路由。 考虑一下如何使用React的声明式可组合性解决问题,因为几乎每个“ React Router问题”都可能是“ React问题”。

测试

React Router依靠React上下文来工作。 这会影响您如何测试在你的组件里使用我们的组件。

Context

如果您尝试对渲染<Link>或<Route>的组件之一进行单元测试,等等。您会收到一些有关上下文的错误和警告。 尽管您可能会想自己亲自设置路由器上下文,我们建议您将单元测试包装在路由器组件之一中:具有history属性的路由或<StaticRouter>,<MemoryRouter>或<BrowserRouter>的基本路由器(如果window.history在测试环境中可作为全局变量使用)。建议使用MemoryRouter或自定义历史记录,以便能够在两次测试之间重置路由器。

class Sidebar extends Component {
  // ...
  render() {
    return (
      <div>
        <button onClick={this.toggleExpand}>expand</button>
        <ul>
          {users.map(user => (
            <li>
              <Link to={user.path}>{user.name}</Link>
            </li>
          ))}
        </ul>
      </div>
    );
  }
}

// broken
test("it expands when the button is clicked", () => {
  render(<Sidebar />);
  click(theButton);
  expect(theThingToBeOpen);
});

// fixed!
test("it expands when the button is clicked", () => {
  render(
    <MemoryRouter>
      <Sidebar />
    </MemoryRouter>
  );
  click(theButton);
  expect(theThingToBeOpen);
});

从指定route开始

<MemoryRouter>支持initialEntries和initialIndex props,因此您可以在特定位置启动应用程序(或应用程序的任何较小部分)。

test("current user is active in sidebar", () => {
  render(
    <MemoryRouter initialEntries={["/users/2"]}>
      <Sidebar />
    </MemoryRouter>
  );
  expectUserToBeActive(2);
});

导航

我们进行了很多测试,以检查route在位置更改时是否有效,因此您可能不需要测试这些东西。 但是,如果您需要在应用程序中测试导航,则可以这样进行:

app.js (a component file)
import React from "react";
import { Route, Link } from "react-router-dom";

// 我们的主题,即应用,但您可以测试任何子项
// 您的应用程序部分
const App = () => (
  <div>
    <Route
      exact
      path="/"
      render={() => (
        <div>
          <h1>Welcome</h1>
        </div>
      )}
    />
    <Route
      path="/dashboard"
      render={() => (
        <div>
          <h1>Dashboard</h1>
          <Link to="/" id="click-me">
            Home
          </Link>
        </div>
      )}
    />
  </div>
);
// 您还可以在此处使用"@testing-library/react"或"enzyme/mount"之类的渲染器
import { render, unmountComponentAtNode } from "react-dom";
import { act } from 'react-dom/test-utils';
import { MemoryRouter } from "react-router-dom";

// app.test.js
it("navigates home when you click the logo", async => {
  // 在真实测试中,渲染器如"@testing-library/react"
  // 将负责设置DOM元素
  const root = document.createElement('div');
  document.body.appendChild(root);

  // Render app
  render(
    <MemoryRouter initialEntries={['/my/initial/route']}>
      <App />
    <MemoryRouter>,
    root
  );

  // 与页面互动
  act(() => {
    // 查找链接(可能使用文本内容)
    const goHomeLink = document.querySelector('#nav-logo-home');
    // Click it
    goHomeLink.dispatchEvent(new MouseEvent("click", { bubbles: true }));
  });

  // 检查显示的页面内容是否正确
  expect(document.body.textContent).toBe('Home');
});

检查测试中的位置

在测试中,您不必经常访问location或history对象,但是如果你这样做了(比如验证在url栏中设置了新的查询参数),你可以在测试中添加一个更新变量的路由:

// app.test.js
test("clicking filter links updates product query params", () => {
  let history, location;
  render(
    <MemoryRouter initialEntries={["/my/initial/route"]}>
      <App />
      <Route
        path="*"
        render={({ history, location }) => {
          history = history;
          location = location;
          return null;
        }}
      />
    </MemoryRouter>,
    node
  );

  act(() => {
    // example: click a <Link> to /products?id=1234
  });

  // assert about url
  expect(location.pathname).toBe("/products");
  const searchParams = new URLSearchParams(location.search);
  expect(searchParams.has("id")).toBe(true);
  expect(searchParams.get("id")).toEqual("1234");
});

备选方案:
1、如果您的测试环境具有浏览器全局变量window.location和window.history(这是通过JSDOM在Jest中的默认设置,但您无法重置测试之间的历史记录),则也可以使用BrowserRouter。
2、您可以将基本路由器与history包中的history props一起使用,而不是将自定义路由传递给MemoryRouter:

// app.test.js
import { createMemoryHistory } from "history";
import { Router } from "react-router";

test("redirects to login page", () => {
  const history = createMemoryHistory();
  render(
    <Router history={history}>
      <App signedInUser={null} />
    </Router>,
    node
  );
  expect(history.location.pathname).toBe("/login");
});

React测试包

请参阅官方文档中的示例:Testing React Router with React Testing Library

Redux集成

Redux是React生态系统的重要组成部分。 对于想要同时使用React Router和Redux的人,我们希望使其无缝集成。

阻止更新

通常,React Router和Redux可以很好地协同工作。不过有时候,应用程序可以包含一个组件,该组件在位置更改时(子routes或活动的导航links不会更新)不会更新。

在以下情况下会发生这种情况:
1、该组件通过connect()(Comp)连接到redux。
2、该组件不是“路由组件”,这意味着它的渲染方式不是这样:<Route component = {SomeConnectedThing} />

问题在于Redux实现了shouldComponentUpdate,如果没有从路由器接收props,则没有任何迹象表明发生了任何变化。
这很容易姐姐,找到连接组件的位置,然后将组件使用withRouter包装在一起

深度集成

有些人想:
1、从store同步路由数据,并从store访问路由数据。
2、可以通过dispatch action操作导航
3、在Redux devtools中支持对路径更改进行时间行程调试。

所有这些都需要更深入的集成。

我们的建议是不要将routes完全保留在Redux store中。论证:
1、路由数据已经成为大多数关心它的组件的支持。 无论是来自store还是router,您组件的代码都基本相同。
2、在大多数情况下,您可以使用Link,NavLink和Redirect执行导航操作。有时您可能还需要以编程方式进行导航,有时您可能还需要以编程方式导航,在某个操作最初启动的异步任务之后。例如,您在用户提交登录表单时调度操作。然后,您的使用thunksaga或其他异步处理程序会对凭据进行身份验证,如果成功,则需要以某种方式导航到新页面。此处的解决方案只是将history对象(提供给所有路由组件)包括在操作的payload,并且异步处理程序可以在适当的时候使用此对象进行导航。
3、路线更改对于时间行程调试不太重要。唯一明显的情况是调试router/store同步中的问题,如果根本不同步它们,则该问题将消失。
但是,如果您强烈希望与store同步route,您可能需要尝试Connected React Router,这是React Router v4和Redux的第三方绑定。

静态Routes

以前版本的React Router使用静态路由来配置应用程序的路由。这样可以在渲染之前检查和匹配路线。由于v4转移到动态组件而不是路由配置,因此一些以前的用例变得不那么明显和棘手。我们正在开发一个可与静态路由配置和React Router配合使用的软件包,以继续满足这些用例。 现在正在开发中,但我们希望您能尝试一下并提供帮助。

React Router Config

API

Hooks

React Router附带了一些钩子,可让您访问路由器的状态并从组件内部执行导航。
请注意:您必须使用React> = 16.8才能使用这些钩子中的任何一个!

useHistory

useHistory钩子使您可以访问可用于导航的history实例。

import { useHistory } from "react-router-dom";

function HomeButton() {
  let history = useHistory();

  function handleClick() {
    history.push("/home");
  }

  return (
    <button type="button" onClick={handleClick}>
      Go home
    </button>
  );
}

useLocation

useLocation钩子返回代表当前URL的location对象。您可以像useState一样考虑它,只要URL更改,它就会返回一个新位置。
这可能非常有用,例如 在您希望每次加载新页面时都使用Web分析工具触发新的"page view"事件的情况下,如以下示例所示:

import React from "react";
import ReactDOM from "react-dom";
import {
  BrowserRouter as Router,
  Switch,
  useLocation
} from "react-router-dom";

function usePageViews() {
  let location = useLocation();
  React.useEffect(() => {
    ga.send(["pageview", location.pathname]);
  }, [location]);
}

function App() {
  usePageViews();
  return <Switch>...</Switch>;
}

ReactDOM.render(
  <Router>
    <App />
  </Router>,
  node
);

useParams

useParams返回URL参数的key/value的对象。 使用它来访问当前<Route>的match.params。

import React from "react";
import ReactDOM from "react-dom";
import {
  BrowserRouter as Router,
  Switch,
  Route,
  useParams
} from "react-router-dom";

function BlogPost() {
  let { slug } = useParams();
  return <div>Now showing post {slug}</div>;
}

ReactDOM.render(
  <Router>
    <Switch>
      <Route exact path="/">
        <HomePage />
      </Route>
      <Route path="/blog/:slug">
        <BlogPost />
      </Route>
    </Switch>
  </Router>,
  node
);

useRouteMatch

useRouteMatch钩子尝试以与<Route>相同的方式匹配当前URL。它主要用于在不实际渲染<Route>的情况下访问匹配数据。
不用useRouteMatch:

import { Route } from "react-router-dom";

function BlogPost() {
  return (
    <Route
      path="/blog/:slug"
      render={({ match }) => {
        // 用match做你想做的一切...
        return <div />;
      }}
    />
  );
}

使用useRouteMatch:

import { useRouteMatch } from "react-router-dom";

function BlogPost() {
  let match = useRouteMatch("/blog/:slug");

  // 用match做你想做的一切...
  return <div />;
}

<BrowserRouter>

一个<Router>,它使用HTML5 history API (pushState、replaceState和popstate事件)来保持UI与URL同步。

<BrowserRouter
  basename={optionalString}
  forceRefresh={optionalBool}
  getUserConfirmation={optionalFunc}
  keyLength={optionalNumber}
>
  <App />
</BrowserRouter>

basename: string

所有location的基本URL。如果您的应用是通过服务器上的子目录提供的,则需要将其设置为子目录。格式正确的basename应以斜杠开头,但不能以斜杠结尾。

getUserConfirmation: func

用于确认导航的功能。 默认使用window.confirm

forceRefresh: bool

如果为true,则路由器将在页面导航中使用整页刷新。您可能希望使用它来模仿传统的服务器渲染应用程序在页面导航之间刷新整个页面的方式。

keyLength: number

location.key的长度。 默认为6。

children: node

要渲染的子元素。
注意:在React <16上,您必须使用单个子元素,因为render方法不能返回多个元素。 如果需要多个元素,则可以尝试将它们包装在额外的<div>中。

<HashRouter>

<Router>使用URL的哈希部分(即window.location.hash)使UI与URL保持同步。
重要说明:Hash history不支持location.key或location.state。在以前的版本中,我们试图纠正这种行为,但是有些边缘情况我们无法解决。任何需要此行为的代码或插件都将无法使用。 由于此技术仅旨在支持旧版浏览器,我们建议您将服务器配置为与<BrowserHistory>一起使用。

<HashRouter
  basename={optionalString}
  getUserConfirmation={optionalFunc}
  hashType={optionalString}
>
  <App />
</HashRouter>

basename: string

所有location的基本URL。 格式正确的basename应以斜杠开头,但不能以斜杠结尾。

<HashRouter basename="/calendar"/>
<Link to="/today"/> // 渲染出来的样子: <a href="#/calendar/today">

getUserConfirmation: func

用于confirm导航的功能。 默认使用window.confirm。

<HashRouter
  getUserConfirmation={(message, callback) => {
    // this is the default behavior
    const allowTransition = window.confirm(message);
    callback(allowTransition);
  }}
/>

hashType: string

用于window.location.hash的编码类型。 可用值为:

  • "slash" - 创建像#/#/sunshine/lollipops的hash
  • "noslash" - 创建像##sunshine/lollipops的hash
  • "hashbang" - 创建诸如#!/#!/sunshine/lollipops之类的"ajax crawlable"(Google弃用)hash

默认为 "/"

children: node

要渲染的单个子元素

import React, { useEffect } from 'react';
import ReactDOM from 'react-dom';
import { HashRouter, Switch, Route, Link, withRouter } from 'react-router-dom';

function INav() {
    let homeRef;
    let anchorRef = React.createRef();
    console.log('before - anchorRef=>', anchorRef);
    useEffect(props => {
        console.log('after - anchorRef=>', anchorRef);
        console.log('after- homeRef=>', homeRef);
    });
    return (
        <ul className={'nav'}>
            <li>
                <Link to={'/home'} replace innerRef={homeRef}>Home</Link>
            </li>
            <li>
                <Link to={'/rule'} innerRef={anchorRef}>Rule</Link>
            </li>
            <li>
                <Link to={'/form'} innerRef={node => {
                    // "node"指的是被挂载的DOM元素
                    // 组件被卸载时为null
                    console.log('node=>', node);
                }}>Form</Link>
            </li>
            <li>
                <Link to={location => `/table?sort=name`}>Table</Link>
            </li>
            <li>
            <Link to={location => {
                console.log('Charts - location=>', location);
                return { ...location, pathname: '/charts' }
            }}>Charts</Link>
            </li>
            <li>
                <Link to={{
                    pathname: '/example',
                    search: '?sort=name',
                    hash: '#the-hash',
                    state: {
                        fromDashboard: true,
                        name: 'Jameswain'
                    }
                }}>Example</Link>
            </li>
        </ul>
    )
}

function Home(props) {
    console.log('Home:', props);
    return <h1>
        Home
    </h1>
}

function Form(props) {
    console.log('Form:', props);
    return <h1>Form</h1>;
}

function Table(props) {
    console.log('Table:', props);
    return <h1>Table</h1>
}

function Rule(props) {
    console.log('rule:', props);
    return <h1>
        Rule
    </h1>
}

const Example = withRouter((props) => {
    console.log('Example:', props);
    return <h1>Example</h1>
});

const Charts = withRouter((props) => {
    console.log('Charts:', props);
    return <h1>Charts</h1>
});


function App() {
    return (
        <HashRouter hashType={'noslash'} basename={'/calendar'}>
            <div className={'app'}>
                <INav/>
                <Switch>
                    <Route path={'/home'} exact>
                        <Home />
                    </Route>
                    <Route path={'/rule'} children={props => <Rule {...props} />} />
                    <Route path={'/form'} render={props => <Form {...props} />} />
                    <Route path={'/table'} component={props => <Table {...props} />} />
                    <Route path={'/charts'} children={<Charts />} />
                    <Route path={'/example'}>
                        <Example />
                    </Route>
                </Switch>
            </div>
        </HashRouter>
    );
}

ReactDOM.render(<App />, document.querySelector('#root'));

<Link>

提供围绕应用程序的声明式、可访问的导航,其实渲染出来的就是一个标签,对标签的封装。

<Link to="/about">About</Link>

to: string

链接位置的字符串表示形式,是通过将location的pathname,search和hash属性连接起来而创建的。

<Link to="/courses?sort=name" />

to: object

可以具有以下任何属性的对象:

  • pathname: 表示要链接到的路径的字符串。
  • search: query参数的字符串表示形式。
  • hash: 网址中的hash值,例如#a-hash。
  • state: 状态保留到该属性中,这个属性设置的内容会被传递到location.state
<Link
  to={{
    pathname: "/courses",
    search: "?sort=name",
    hash: "#the-hash",
    state: { fromDashboard: true }
  }}
/>

to: function

将当前位置作为参数传递给它的函数,该函数应该以字符串或对象的形式返回位置信息

<Link to={location => ({ ...location, pathname: "/courses" })} />
<Link to={location => `${location.pathname}?sort=name`} />

replace: bool

如果为true,则将单击链接替换为history记录堆栈中的当前条目,而不是添加一条新条目。
这样就没有回退功能了,因为它是把当前URL地址替换掉,不会产生历史记录。

<Link to="/courses" replace />

innerRef: function

从React Router 5.1开始,如果您使用的是React16,则不需要此props,因为我们会将ref转发到基础。允许访问组件的基础引用。

<Link
  to="/"
  innerRef={node => {
    // “node”指的是被挂载的DOM元素
    // 组件被卸载时为null
  }}
/>

innerRef: RefObject

从React Router 5.1开始,如果您使用的是React16,则不需要此props,因为我们会将ref转发到基础使用React.createRef获取组件的基础引用。

let anchorRef = React.createRef()
<Link to="/" innerRef={anchorRef} />

其他

您还可以传递想要在上显示的props,例如title,id,className等。

<NavLink>

<Link>的特殊版本,当它与当前URL匹配时,它将为渲染的元素添加样式属性。

<NavLink to="/about">About</NavLink>

activeClassName: string

当元素处于active时给该元素设置的class,默认给定的class是active的,这将与className属性连接在一起。

<NavLink to="/faq" activeClassName="selected">
  FAQs
</NavLink>

activeStyle: object

元素处于active状态时应用于该元素的样式。

<NavLink
  to="/faq"
  activeStyle={{
    fontWeight: "bold",
    color: "red"
  }}
>
  FAQs
</NavLink>

exact: bool

如果为true,则仅在locatiuon完全匹配时才应用active的class或style。

<NavLink exact to="/profile">
  Profile
</NavLink>

strict: bool

如果为true,则在确定位置是否与当前URL匹配时,将会考虑位置路径名上的斜杠,它需要和<Route>配合使用。有关更多信息,请参见<Route strict>文档。

// 严格模式,无法匹配,URL必须要一模一样才能匹配上
<NavLink strict to="/events">
  Events
</NavLink>
<Switch>
    <Route path={'/events/'} strict children={<Events />} />
</Switch>

isActive: func

一种添加额外逻辑以确定链接是否处于active状态的功能。如果您要做的事情不仅仅是验证链接的路径名是否与当前URL的路径名匹配,则可以使用此选项。

<NavLink
  to="/events/123"
  isActive={(match, location) => {
    if (!match) {
      return false;
    }

    // 仅当事件id为奇数时元素才为active状态
    const eventID = parseInt(match.params.eventID);
    return !isNaN(eventID) && eventID % 2 === 1;
  }}
>
  Event 123
</NavLink>

location: object

isActive比较当前历史记录位置(通常是当前浏览器URL)。如果要与其他location进行比较,可以传递一个位置。

aria-current: string

在active链接上使用的aria-current属性的值。可用值为:

  • "page"- 用于指示一组分页链接中的链接
  • "step"- 用于指示基于步骤的过程的步骤指示器中的链接
  • "location"- 用于指示视觉上突出显示的图像作为流程图的当前组成部分
  • "date"- 用于指示日历中的当前日期
  • "time"- 用于指示时间表中的当前时间
  • "true"- 用于指示NavLink是否处于活动状态

默认值为 "page"
基于WAI-ARIA 1.1规范

import React from 'react';
import ReactDOM from 'react-dom';
import { HashRouter, Switch, Route, NavLink } from 'react-router-dom';

function Home() {
    return <h1>Home</h1>
}

function About() {
    return <h1>About</h1>
}

const Charts = () => <h1>Charts</h1>;
const Table = () => <h1>Table</h1>;
const FAQ = () => <h1>FAQ</h1>;
const Events = () => <h1>Events</h1>;

function App() {
    return <div className={'app'}>
        <HashRouter hashType={'noslash'}>
            <ul>
                <li>
                    <NavLink to={'/home'} className={'home'}>Home</NavLink>
                </li>
                <li>
                    <NavLink to={'/about'} className={'about'}>About</NavLink>
                </li>
                <li>
                    <NavLink to={'/charts'} className={'charts'} activeClassName={'selected'}>Charts</NavLink>
                </li>
                <li>
                    <NavLink to={'/table'} className={'table'} activeClassName={'selected'}>Table</NavLink>
                </li>
                <li>
                    <NavLink to={'/faq'} activeStyle={{ fontWeight: 'bold', color: 'red' }}>FAQ</NavLink>
                </li>
                <li>
                    <NavLink strict to="/events">Events</NavLink>
                </li>
            </ul>
            <Switch>
                <Route path={'/home'} children={<Home/>} />
                <Route path={'/about'} children={<About/>} />
                <Route path={'/charts'} children={<Charts/>} />
                <Route path={'/table'} children={<Table/>} />
                <Route path={'/faq'} children={<FAQ />} />
                <Route path={'/events/'} strict children={<Events />} />
            </Switch>
        </HashRouter>
    </div>
}

ReactDOM.render(<App />, document.querySelector('#root'))

<Prompt>

用于在离开页面之前提示用户。当您的应用程序进入应阻止用户导航的状态时(例如,表单已被半填满),请渲染<Prompt>。

<Prompt
  when={formIsHalfFilledOut}
  message="您确定要离开吗?"
/>

message: string

当用户尝试离开时提示用户的消息。

<Prompt message="Are you sure you want to leave?" />

message: func

将与用户尝试导航到的下一个位置和操作一起调用。返回一个字符串以向用户显示提示,或者返回true以允许过渡。

<Prompt
  message={location =>
    location.pathname.startsWith("/app")
      ? true
      : `Are you sure you want to go to ${location.pathname}?`
  }
/>

when: bool

您可以始终渲染它,而可以通过when={true}或when={false}来阻止或允许进行相应的导航,而不是通过条件控制是否渲染<Prompt>。

<Prompt when={formIsHalfFilledOut} message="Are you sure?" />
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter, Switch, Route, Link, Prompt } from 'react-router-dom';

function Home() {
    return <h1>Home</h1>
}

function Table() {
    return <h1>Table</h1>;
}

function Charts() {
    return <h1>Charts</h1>
}

function About() {
    return <h1>About</h1>
}

function App() {
    return <BrowserRouter>
        <Prompt message={location => {
            if (location.pathname !== '/home') {
                return `您确定要前往${location.pathname}吗?`
            } else {
                return true;
            }
            return true;
        }} when={true} />
        <ul>
            <li>
                <Link to={'/home'}>Home</Link>
            </li>
            <li>
                <Link to={'/table'}>Table</Link>
            </li>
            <li>
                <Link to={'/charts'}>Charts</Link>
            </li>
            <li>
                <Link to={'/about'}>About</Link>
            </li>
        </ul>
        <Switch>
            <Route path={'/home'} children={props => <Home {...props} />} />
            <Route path={'/table'} render={props => <Table {...props} />} />
            <Route path={'/charts'} children={props => <Charts {...props} />} />
            <Route path={'/about'} render={props => <About {...props} />} />
        </Switch>
    </BrowserRouter>;
}

ReactDOM.render(<App />, document.querySelector('#root'));

<Redirect>

渲染<Redirect>将导航到新位置。新位置将覆盖历史记录堆栈中的当前位置,就像服务器端重定向(HTTP 3xx)一样。

<Route exact path="/">
  {loggedIn ? <Redirect to="/dashboard" /> : <PublicHomePage />}
</Route>

to: string

重定向到的URL。path-to-regexp@^1.7.0可以理解的任何有效URL路径。to中使用的所有URL参数必须由from覆盖。

<Redirect to="/somewhere/else" />

to: object

重定向到的位置。路径名可以是path-to-regexp@^1.7.0可以理解的任何有效URL路径。

<Redirect
  to={{
    pathname: "/login",
    search: "?utm=your+face",
    state: { referrer: currentLocation }
  }}
/>

可以通过重定向到组件中的this.props.location.state访问状态对象。然后,可以通过路径名"/login"指向的Login组件中的this.props.location.state.referrer访问此新的引用关键字(不是特殊名称)。

push: bool

<Redirect push to="/somewhere/else" />

设置为true时,重定向会将新条目推入历史记录,而不是替换当前条目。

from: string

要重定向的路径名。 path-to-regexp@^1.7.0可以理解的任何有效URL路径。所有匹配的URL参数都提供给模式中的to。必须包含用于to中的所有参数。to不使用的其他参数将被忽略。

<Switch>
  <Redirect from='/old-path' to='/new-path' />
  <Route path='/new-path'>
    <Place />
  </Route>
</Switch>

// 使用匹配的参数重定向
<Switch>
  <Redirect from='/users/:id' to='/users/profile/:id'/>
  <Route path='/users/profile/:id'>
    <Profile />
  </Route>
</Switch>

示例:

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter, Switch, Route, Redirect, Link, useParams } from 'react-router-dom';

function App() {
    return(
        <BrowserRouter>
            <ul>
                <li>
                    <Link to={'/home'}>Home</Link>
                </li>
                <li>
                    <Link to={'/charts/123123'}>Charts</Link>
                </li>
                <li>
                    <Link to={'/profile/111'}>Profile</Link>
                </li>
            </ul>
            <Switch>
                <Route path={'/home'} render={props => <Home {...props} />} />
                <Route path={'/profile/:id'} render={props => <Profile {...props} />} />
                <Redirect from={'/charts/:id'} to={'/profile/:id'} />
            </Switch>
        </BrowserRouter>
    )
}
function Home() {
    return <h1>Home</h1>
}
function Profile() {
    const params = useParams();
    console.log('params=>', params);
    return <>
        <h1>Profile</h1>
    </>
}
ReactDOM.render(<App />, document.querySelector('#root'));

exact: bool

完全匹配;等同于Route.exact
注意:只有在<Switch>内渲染<Redirect>时,才能与from结合使用,以完全匹配位置。有关更多详细信息,请参见<Switch children>

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter, Switch, Route, Link, Redirect } from 'react-router-dom';

const Home = () => <h1>Home</h1>;
const About = () => <h1>About</h1>;
const App = () => <BrowserRouter>
    <ul>
        <li>
            <Link to={'/home'}>Home</Link>
        </li>
        <li>
            <Link to={'/about'}>About</Link>
        </li>
    </ul>
    <Switch>
        <Route path={'/home'} render={props => <Home {...props} />} />
        <Route path={'/about'} children={props => <About {...props} />} />
        {/*这个一定要放到Route后面,等Route渲染完了,才可以重定向*/}
        <Redirect exact from={'/'} to={'/home'} />
    </Switch>
</BrowserRouter>;

ReactDOM.render(<App />, document.querySelector('#root'));

strict: bool

严格匹配;等同于Route.strict
注意:只有在<Switch>内部渲染<Redirect>时,此选项只有与from一起使用才能以严格匹配位置。有关更多详细信息,请参见<Switch children>

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter, Switch, Route, Link, Redirect } from 'react-router-dom';

const Home = () => <h1>Home</h1>;
const About = () => <h1>About</h1>;
const App = () => <BrowserRouter>
    <ul>
        <li>
            <Link to={'/home'}>Home</Link>
        </li>
        <li>
            <Link to={'/about'}>About</Link>
        </li>
        <li>
            <Link to={'/one'}>One</Link>
        </li>
    </ul>
    <Switch>
        <Route path={'/home'} render={props => <Home {...props} />} />
        <Route path={'/about'} children={props => <About {...props} />} />
        {/*这个一定要放到Route后面,等Route渲染完了,才可以重定向*/}
        <Redirect strict from="/one/" to="/home" />
    </Switch>
</BrowserRouter>;

ReactDOM.render(<App />, document.querySelector('#root'));

sensitive: bool

区分大小写匹配;等同于Route.sensitive

<Route sensitive path="/one">
  <About />
</Route>
pathlocation.pathnamesensitive是否匹配
/one/onetrueyes
/One/onetrueno
/One/onefalseyes

<Router>

Router组件可能是React Router中了解和学习使用的最重要组件。它的最基本职责是在其路径与当前URL匹配时显示一些UI。
研究以下代码:

import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter as Router, Route } from "react-router-dom";

ReactDOM.render(
  <Router>
    <div>
      <Route exact path="/">
        <Home />
      </Route>
      <Route path="/news">
        <NewsFeed />
      </Route>
    </div>
  </Router>,
  node
);

如果应用程序的location是/,则UI层次结构将类似于:

<div>
  <Home />
  <!-- react-empty: 2 -->
</div>

如果应用程序的location是/news,则UI层次结构将是:

<div>
  <!-- react-empty: 1 -->
  <NewsFeed />
</div>

"react-empty"注释只是React空渲染的实现细节。但是出于我们的目的,这是有益的。从技术上讲,即使始终为空,也总是对其进行"渲染"。当<Route>的路径与当前URL匹配时,它将渲染其子级(您的组件)。

Route render methods

使用<Route>渲染某些内容的方法建议使用子元素,如上所示。 但是,还有一些其他方法可用于使用<Route>渲染内容。 提供这些主要是为了支持在引入钩子之前使用早期版本的路由器构建的应用程序。

您应该在给定的<Route>上仅使用这些props。 请参阅下面的说明以了解它们之间的区别。

Route props

所有这三种渲染方法将通过相同的三个路由props

component

一个仅在location匹配时才渲染的React组件。 它将与route props一起渲染。

import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter as Router, Route } from "react-router-dom";

// 用户可以使用所有route props(match, location and history)
function User(props) {
  return <h1>Hello {props.match.params.username}!</h1>;
}

ReactDOM.render(
  <Router>
    <Route path="/user/:username" component={User} />
  </Router>,
  node
);

当您使用组件(而不是下面的渲染器或子组件)时,路由器会使用React.createElement从给定的组件中创建一个新的React元素。这意味着,如果您向组件prop提供内联函数,则将在每个渲染中创建一个新组件。这意味着,如果您向组件prop提供内联函数,则将在每个渲染中创建一个新组件。这将导致现有组件的卸载和新组件的安装,而不仅仅是更新现有组件。使用内联函数进行内联渲染时,请使用render或children属性(如下)。

render: func

这样可以方便地进行内联渲染和包装,而无需进行上述不必要的重新安装。
无需使用组件prop为您创建新的React元素,而是可以传递位置匹配时要调用的函数。render函数可以访问与组件渲染相同的所有route属性(match,location和history)。

import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter as Router, Route } from "react-router-dom";

// 方便的内联渲染
ReactDOM.render(
  <Router>
    <Route path="/home" render={props => <div>Home</div>} />
  </Router>,
  node
);
import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter as Router, Route } from "react-router-dom";
// wrapping/composing
// 您可以传播 route 属性 以使它们可用于渲染的组件
function FadingRoute({ component: Component, ...rest }) {
  return (
    <Route
      {...rest}
      render={routeProps => (
        <FadeIn>
          <Component {...routeProps} />
        </FadeIn>
      )}
    />
  );
}

ReactDOM.render(
  <Router>
    <FadingRoute path="/cool" component={Something} />
  </Router>,
  node
);

警告:<Route component>优先于<Route render>,因此请勿在同一<Route>中同时使用两者。

children: func

有时您需要渲染路径是否与位置匹配。 在这种情况下,您可以使用child道具功能。 它与render完全一样,除了是否存在匹配项而被调用。
子级渲染属性接收组件渲染函数相同的所有路由属性,除非路由未能与URL匹配,则match为null。 这样您可以根据路由是否匹配来动态调整UI。 如果路由匹配,我们在这里添加一个active class。

import React from "react";
import ReactDOM from "react-dom";
import {
  BrowserRouter as Router,
  Link,
  Route
} from "react-router-dom";

function ListItemLink({ to, ...rest }) {
  return (
    <Route
      path={to}
      children={({ match }) => (
        <li className={match ? "active" : ""}>
          <Link to={to} {...rest} />
        </li>
      )}
    />
  );
}

ReactDOM.render(
  <Router>
    <ul>
      <ListItemLink to="/somewhere" />
      <ListItemLink to="/somewhere-else" />
    </ul>
  </Router>,
  node
);

这对于动画也可能有用:

<Route
  children={({ match, ...rest }) => (
    {/* Animate将始终进行渲染,因此您可以使用生命周期来对其子对象进行动画制作 */}
    <Animate>
      {match && <Something {...rest}/>}
    </Animate>
  )}
/>

警告:<Route children>优先于<Route component>和<Route render>,因此请不要在同一<Route>中使用多个。

path: string | string[]

如果为true,则仅在路径与location.pathname完全匹配时才匹配。

<Route exact path="/one">
  <About />
</Route>
pathlocation.pathnameexact是否匹配
/one/one/twotrueno
/one/one/twofalseyes

strict: bool

设置为true时,带有斜杠的路径将只匹配带有斜杠的location.pathname。当location.pathname中有其他URL段时,这无效。

<Route strict path="/one/">
  <About />
</Route>
path    
pathlocation.pathname是否匹配
/one//oneno
/one//one/yes
/one//one/twoyes

警告:strict可以用于强制location.pathname不带斜杠,但是要做到这一点,strict和exact都必须为true。

<Route exact strict path="/one">
  <About />
</Route>
pathlocation.pathname是否匹配
/one/oneyes
/one/one/no
/one/one/twono

location: object

<Route>元素尝试将其路径与当前历史记录位置(通常是当前浏览器URL)匹配。但是,也可以传递路径名不同的位置进行匹配。

在需要将<Route>匹配到当前历史记录位置以外的位置时,这很有用,如Animated Transitions示例所示。

如果<Route>元素包装在<Switch>中并且与传递给<Switch>的位置(或当前历史记录位置)匹配,则传递给<Route>位置的prop将被<Switch>使用的那个props覆盖(此处给出)。

sensitive: bool

为true时,如果路径区分大小写,则将匹配。

<Route sensitive path="/one">
  <About />
</Route>
pathlocation.pathnamesensitive是否匹配
/one/onetrueyes
/One/onetrueno
/One/onefalseyes

<Router>

所有路由器组件的通用底层接口。通常,应用将使用高级路由器之一代替:

使用底层<Router>的最常见用例是将自定义历史记录与状态管理库(如Redux或Mobx)进行同步。 请注意,并不需要将状态管理库与React Router一起使用,它仅用于深度集成。

import React from "react";
import ReactDOM from "react-dom";
import { Router } from "react-router";
import { createBrowserHistory } from "history";

const history = createBrowserHistory();

ReactDOM.render(
  <Router history={history}>
    <App />
  </Router>,
  node
);

history: object

用于导航的history对象

import React from "react";
import ReactDOM from "react-dom";
import { createBrowserHistory } from "history";

const customHistory = createBrowserHistory();

ReactDOM.render(<Router history={customHistory} />, node);

children: node

要渲染的子元素。

<Router>
  <App />
</Router>

<StaticRouter>

永远不会更改位置的<Router>。

当用户实际上没有四处点击时,这在服务器端渲染方案中很有用,因此位置永远不会发生实际变化。 因此,名称为:static。它在简单测试中也很有用,您只需要插入一个位置并在渲染输出中进行断言时。

示例:这是一个node服务器,它为<Redirect>发送302状态代码,并为其他请求发送常规HTML:

requests:import http from "http";
import React from "react";
import ReactDOMServer from "react-dom/server";
import { StaticRouter } from "react-router";

http
  .createServer((req, res) => {
    // This context object contains the results of the render
    const context = {};

    const html = ReactDOMServer.renderToString(
      <StaticRouter location={req.url} context={context}>
        <App />
      </StaticRouter>
    );

    // context.url will contain the URL to redirect to if a <Redirect> was used
    if (context.url) {
      res.writeHead(302, {
        Location: context.url
      });
      res.end();
    } else {
      res.write(html);
      res.end();
    }
  })
  .listen(3000);

basename: string

所有位置的基本URL。格式正确的基本名称应以斜杠开头,但不能以斜杠结尾。

<StaticRouter basename="/calendar">
  <Link to="/today"/> // renders <a href="/calendar/today">
</StaticRouter>

location: string

服务器收到的URL,可能是node服务器上的req.url.

<StaticRouter location={req.url}>
  <App />
</StaticRouter>

location: object

形状为{ pathname, search, hash, state }的location对象

<StaticRouter location={{ pathname: "/bubblegum" }}>
  <App />
</StaticRouter>

context: object

一个普通的JavaScript对象。在渲染期间,组件可以向对象添加属性以存储有关渲染的信息

const context = {}
<StaticRouter context={context}>
  <App />
</StaticRouter>

当<Route>匹配时,它将把上下文对象传递给它作为staticContext属性呈现的组件。请查看服务器渲染指南,以获取有关如何自行执行此操作的更多信息。

渲染后,这些属性可用于配置服务器的响应。

if (context.status === "404") {
  // ...
}

children: node

要渲染的子元素。

注意:在React <16上,您必须使用单个子元素,因为render方法不能返回多个元素。如果需要多个元素,则可以尝试将它们包装在额外的<div>

<Switch>

渲染与位置匹配的第一个子元素<Route>或<Redirect>。
这与仅使用一堆<Route>有什么不同?
<Switch>的独特之处在于它专门渲染一条路由。相反,每个与该位置匹配的<Route>都将进行包含性渲染。研究以下route:

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter, Route } from 'react-router-dom';

function SwitchExample() {
    return <BrowserRouter>
        <Route path="/about">
            <h1>About</h1>
        </Route>
        <Route path="/:user">
            <h1>User</h1>
        </Route>
        <Route>
            <h1>NoMatch</h1>
        </Route>
    </BrowserRouter>;
}

ReactDOM.render(<SwitchExample />, document.querySelector('#root'));

如果URL是/about,则渲染<About>,<User>和<NoMatch>将全部渲染,因为它们都与所有路径都匹配。这是设计使然,允许我们以多种方式将<Route>组合到我们的应用中,例如边栏和面包屑,引导程序标签等。

但是,有时我们只选择一个<Route>进行渲染。如果我们位于/about,我们不想同时匹配/:user(或显示"404"页面)。使用Switch的方法如下:

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter, Switch, Route } from 'react-router-dom';

const Home = () => <h1>Home</h1>;
const About = () => <h1>About</h1>;
const User = () => <h1>User</h1>;
const NoMatch = () => <h1>NoMatch</h1>;

function SwitchExample() {
    return <BrowserRouter>
        <Switch>
            <Route exact path='/'>
                <Home />
            </Route>
            <Route path='/about'>
                <About />
            </Route>
            <Route path='/:user'>
                <User />
            </Route>
            <Route>
                <NoMatch />
            </Route>
        </Switch>
    </BrowserRouter>
}

ReactDOM.render(<SwitchExample />, document.querySelector('#root'));

现在,如果我们位于/about,<Switch>将开始寻找匹配的<Route>。<Route path ="/about" />将匹配,而<Switch>将停止寻找匹配并渲染<About>。同样,如果我们在/michael位置,则会显示<User>。

这对于动画过渡也很有用,因为匹配的<Route>呈现在与上一个相同的位置。

let routes = (
  <Fade>
    <Switch>
      {/* 这里只有一个子元素 */}
      <Route />
      <Route />
    </Switch>
  </Fade>
);

let routes = (
  <Fade>
    {/* 这里永远有两个子元素,但是可能会呈现null,进行转换,计算起来有点麻烦 */}
    <Route />
    <Route />
  </Fade>
);

location: object

用于匹配子元素的位置对象,而不是当前历史记录位置(通常是当前浏览器URL)。

children: node

<Switch>的所有子代应为<Route>或<Redirect>元素。仅第一个与当前位置匹配的子元素会被渲染。
<Route>元素使用其path属性进行匹配,而<Redirect>元素使用其from属性进行匹配。没有path属性的<Route>或没有from属性的<Redirect>将始终与当前位置匹配。
在<Switch>中包含<Redirect>时,它可以使用<Route>的任何位置匹配属性:path,exact和strict。 from只是path属性的别名。
如果给<Switch>一个location属性,它将覆盖匹配的子元素上的location属性。

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter, Route, Switch, Redirect } from 'react-router-dom';

const Home = () => <h1>Home</h1>;
const User = () => <h1>User</h1>;
const NoMatch = () => <h1>NoMatch</h1>;

function SwitchExample() {
    return <BrowserRouter>
        <Switch>
            <Route exact path="/">
                <Home />
            </Route>
            <Route path="/user">
                <User />
            </Route>
            <Redirect from="/account" to="/user" />
            <Route>
                <NoMatch /> 
            </Route>
        </Switch>
    </BrowserRouter>;
}

ReactDOM.render(<SwitchExample />, document.querySelector('#root'));

history

本文档中的"history"和"history对象"一词是指history包,它是React Router仅有的两个主要依赖项之一(除了React本身)并且提供了几种不同的实现,用于在各种环境中管理JavaScript中的会话历史记录。

也使用以下术语:

  • "browser history" - 特定于DOM的实现,在支持HTML5历史记录API的Web浏览器中很有用.
  • "hash history" - 遗留Web浏览器的DOM特定实现.
  • "memory history" - 内存历史记录实现,可用于测试和像React Native这样的非DOM环境.

history对象通常具有以下属性和方法:

  • length -(number)历史记录堆栈中的条目数
  • action - (string)当前操作(PUSH,REPLACE或POP)
  • location - (object)当前位置。可能具有以下属性:

    • pathname - (string)URL的路径
    • search - (string)URL查询字符串
    • hash - (string)URL哈希片段
    • state - (object)提供给例如当此位置被压入堆栈时,push(path,state)。仅在browser和memory history中可用。
  • push(path, [state]) - (function)将新条目推入历史记录堆栈
  • replace(path, [state]) - (function)替换历史记录堆栈上的当前条目
  • go(n) - (function)将历史记录堆栈中的指针移动n个条目
  • goBack() - (function)相当于go(-1)
  • goForward() - (function)相当于go(1)
  • block(prompt) - (function)防止导航(请参阅history文档

history是可变的

history对象是可变的,因此,建议从<Route>的渲染属性中访问location,而不是从history.location中访问。这确保了您对React的假设在生命周期钩子中是正确的。例如:

class Comp extends React.Component {
  componentDidUpdate(prevProps) {
    // 将为 true
    const locationChanged =
      this.props.location !== prevProps.location;

    // 不正确,由于history是可变的,因此*总是*为假。
    const locationChanged =
      this.props.history.location !== prevProps.history.location;
  }
}

<Route component={Comp} />;

根据您所使用的实现方式,可能还会显示其他属性。请参阅history文档以获取更多详细信息。

location

location表示该应用程序现在的位置,您希望其运行的位置,甚至是以前的位置。看起来像这样:

{
  key: 'ac3df4', // not with HashHistory!
  pathname: '/somewhere',
  search: '?some=search-string',
  hash: '#howdy',
  state: {
    [userDefined]: true
  }
}

router将在几个地方为您提供location对象:

也可以在history.location上找到它,但是您不应使用它,因为它是可变的。您可以在history文档
中阅读有关此内容的更多信息.

location对象永远不会发生变化,因此您可以在生命周期钩子中使用它来确定何时进行导航,这对于数据获取和动画处理非常有用。

componentWillReceiveProps(nextProps) {
  if (nextProps.location !== this.props.location) {
    // navigated!
  }
}

你可以提供位置,而不是字符串导航到不同的地方:

通常您只需要使用一个字符串,但是,如果您需要添加一些“位置状态”,只要应用返回到该特定位置即可使用,则可以使用location对象代替。如果您要基于导航历史而不是仅基于路径(如 modals)来分支UI,这将非常有用。

// 通常你所需要的
<Link to="/somewhere"/>

// 但是你可以使用location来代替
const location = {
  pathname: '/somewhere',
  state: { fromDashboard: true }
}

<Link to={location}/>
<Redirect to={location}/>
history.push(location)
history.replace(location)

最后,您可以将location传递给以下组件:

这样可以防止他们在路由器状态下使用实际位置。这对于动画和待处理的导航很有用,或者在您想要诱使组件在与真实位置不同的位置进行渲染时,这很有用。

match

match对象包含有关<Route path>如何与URL匹配的信息。匹配对象包含以下属性:

  • params- (object)从与路径的动态段相对应的URL解析的键/值对
  • isExact- (boolean)如果整个网址都匹配,则为“ true”(不包含结尾字符)
  • path- (string) 用于匹配的路径模式。对于构建嵌套的<Route>有用
  • url- (string) URL的匹配部分。对于构建嵌套的<Link>有用

您将可以在各个地方使用match对象:

如果Route没有path,因此会始终匹配,它将获取最接近的父项匹配项。和withRouter一样。

null matches

即使子路径的路径与当前位置不匹配,使用子项道具的<Route>也会调用其子函数。 在这种情况下,匹配将为空。 能够在匹配时呈现<Route>的内容可能会很有用,但是这种情况会带来一些挑战。

"解析"URL的默认方法是将match.url字符串连接到"相对"路径。

let path = `${match.url}/relative-path`;

如果在匹配为null时尝试执行此操作,则最终将出现TypeError。这意味着在使用children prop时尝试在<Route>内部加入"relative"路径是不安全的。

当在生成空匹配对象的<Route>中使用无路径<Route>时,会发生类似但更微妙的情况。

// location.pathname = '/matches'
<Route path="/does-not-match"
  children={({ match }) => (
    // match === null
    <Route
      render={({ match: pathlessMatch }) => (
        // pathlessMatch === ???
      )}
    />
  )}
/>

无路径<Route>从其父级继承其match对象。如果其父匹配项为null,则其匹配项也将为null。这意味着任何子级路由/链接都必须是绝对的,因为没有父级可以解析,并且父级匹配可以为null的无路径路由将需要使用子级prop进行渲染。

matchPath

这允许您使用与<Route>相同的匹配代码(除了正常的渲染周期之外),比如在服务器上呈现之前收集数据依赖关系。

import { matchPath } from "react-router";

const match = matchPath("/users/123", {
  path: "/users/:id",
  exact: true,
  strict: false
});

pathname

第一个参数是您要匹配的路径名。如果您是在Node.js的服务器上使用它,则为req.path。

props

第二个参数是要匹配的props,它们与Route接受的匹配props相同。它也可以是字符串或字符串数​​组,作为{path}的快捷方式:

{
  path, // 像/users/:id;单个字符串或字符串数​​组
  strict, // 可选,默认为false
  exact, // 可选,默认为false
}

returns

当提供的路径名与路径属性匹配时,它将返回一个对象。

matchPath("/users/2", {
  path: "/users/:id",
  exact: true,
  strict: true
});

//  {
//    isExact: true
//    params: {
//        id: "2"
//    }
//    path: "/users/:id"
//    url: "/users/2"
//  }

如果提供的路径名与路径属性不匹配,则返回null。

matchPath("/users", {
  path: "/users/:id",
  exact: true,
  strict: true
});

//  null

withRouter

您可以通过withRouter高阶组件访问history对象的属性和最接近的<Route>匹配项。每当渲染时,withRouter都会将更新的match,location和history属性传递给包装的组件。

import React from "react";
import PropTypes from "prop-types";
import { withRouter } from "react-router";

// 一个简单的组件,显示当前位置的路径名
class ShowTheLocation extends React.Component {
  static propTypes = {
    match: PropTypes.object.isRequired,
    location: PropTypes.object.isRequired,
    history: PropTypes.object.isRequired
  };

  render() {
    const { match, location, history } = this.props;

    return <div>You are now at {location.pathname}</div>;
  }
}

// 创建一个“连接”到路由器的新组件(借用redux术语)。
const ShowTheLocationWithRouter = withRouter(ShowTheLocation);

重要的提示:
withRouter不像React Redux的connect那样订阅位置更改以进行状态更改。而是在位置更改后从<Router>组件传播出去后重新渲染。这意味着withRouter不会在路由转换时重新渲染,除非其父组件重新渲染。
静态方法和属性
包装组件的所有非特定于反应的静态方法和属性将自动复制到“connected”组件。

Component.WrappedComponent

包装的组件在返回的组件上作为静态属性WrappedComponent公开,它可以用于隔离测试组件等。

// MyComponent.js
export default withRouter(MyComponent)

// MyComponent.test.js
import MyComponent from './MyComponent'
render(<MyComponent.WrappedComponent location={{...}} ... />)

wrappedComponentRef: func

该函数将作为ref prop传递给包装的组件。

class Container extends React.Component {
  componentDidMount() {
    this.component.doSomething();
  }

  render() {
    return (
      <MyComponent wrappedComponentRef={c => (this.component = c)} />
    );
  }
}
查看原文

赞 52 收藏 34 评论 2

认证与成就

  • 获得 26 次点赞
  • 获得 5 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 5 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-07-22
个人主页被 1.2k 人浏览