前言
本文是为了记录作者暑期实习学习到的知识,对其进行了总结和思考,分享出来,希望则可以帮助到像作者一样还是小白的uu。
开发者工具(Chrome)
主要是一些谷歌的一些快捷键,刚开始用的时候,记不住很正常,多用几次就好啦。
操作 | windows快捷键 |
---|---|
打开一个新窗口 | ctrl+n |
打开新的标签页并跳转到新的标签页 | ctrl+t |
跳转到特定标签页 | ctrl+1到ctrl+8 |
跳转到最后一个标签页 | ctrl+9 |
打开当前标签页浏览记录中记录的上\下一个页面 | alt+向左箭头/alt+向右箭头 |
关闭当前窗口 | ctrl+w |
跳转到下一个打开的标签页 | Ctrl + Tab |
跳转到上一个打开的标签页 | Ctrl + Shift + Tab |
清晰的知道控制台下各个模块的作用,对代码的调试是很有益处的。这里贴一个链接,方便以后查看。
bash
https://juejin.cn/book/6844733783166418958/section/6844733783187390477
whistle
以我目前的水平,主要还是利用whistle搭配SwitchyOmega对接口进行测试以及代理。别的功能还需要在以后的开发过程中慢慢深入的学习。
测试接口的流程
- w2 start启动whistle
- 新建规则,并应用规则。
- 新建values,这一步不是必须的,只是为了以防后端接口还没写好,但此时又想测试接口,我们局可以自己新建数据文件,当访问接口时,返回我们事先准备好的数据。
- 查看请求情况
whistle的安装流程甩个链接在这里: juejin.cn/post/686188...
git的使用
git下载
txt
https://blog.csdn.net/rej177/article/details/126998371
新建一个Git的两种方式
1.本地新建好 Git 项目,然后关联远程仓库
git
# 初始化一个Git仓库
git init
# 关联远程仓库
git remote add <name> <git-repo-url>
# 例如:
git remote add origin https://github.com/xxxxxxxxx
2.clone远程仓库
git
# 在远程新建好仓库,然后clone到本地
git clone <git-repo-url>
# 将远程仓库下载到(当前 git bash 启动位置下面的)指定文件中,如果没有会自动生成
git clone <git-repo-url> <project-name>
clone仓库代码
在使用ssh进行clone之前,还需要进行如下配置:
- 在bash.exe中生成密钥:
ssh-keygen -t ed25519 -C "你自己的邮箱"
,一路回车完成密钥的生成。 - 后台启动ssh代理:eval "$(ssh-agent -s)"
- 添加密钥到对应账号的ssh密钥中(我使用的是gitee),可以通过输入
cat ~/.ssh/id_ed25519.pub
获取ssh密钥,也可以通过记事本打开id_ed25519.pub
文件,将内容全部复制并粘贴。
1.使用https进行clone
-
先创建一个文件夹用来接收克隆下来的代码
-
cmd--->使用命令git clone url
-
代码克隆完成
2.使用ssh进行clone
-
创建文件夹用来接收克隆修改来的代码
-
cmd--->使用命令git clone url
-
代码克隆完成
-
使用ssh克隆代码报错
-
使用git拉代码时报错: Unable to negotiate with **** port 22: no matching host key type found.
txt
原因:可能是新的ssh客户端不支持ssh-rsa算法,要修改本地配置重新使用ssh-rsa算法。
解决办法:在生成公钥的文件夹里(一般在当前用户目录下的.ssh文件中)创建一个config文件(没有后缀),用文本文档格式打开,添加下方内容
Host *
HostkeyAlgorithms +ssh-rsa
PubkeyAcceptedKeyTypes +ssh-rsa
一些常用的git命令
设置用户名和邮箱
cmd
git config --global user.name "John Doe"
git config --global user.email johndoe@example.com
拷贝仓库代码
cmd
git clone url
分支相关的命令
git
# 查看本地所有的分支
git branch
# 查看远程所有的分支
git branch -r
# 查看所有的分支,本扩本地分支和远程分支
git branch -a
# 切换分支
git checkout branchName
# 将本地仓库文件推送到指定的远程分支
git push -o origin branchName
回滚操作
git
# 把指定文件从暂存区回滚到工作区
git restore --staged <file>
# 把指定文件的更改撤销
git restore <file>
# 回滚到最近一次提交的上一个版本
git checkout HEAD^
# 回滚到最近一次提交的上两个版本
git checkout HEAD^^
上面总结的是作者用的比较多的命令,更多用法可以去查看下面的文章,写的非常详细。
txt
https://juejin.cn/post/6844904191203213326#heading-26
TypeScript
1.Typescript开发环境搭建
1.下载node.js
2.安装node.js
3.使用npm全局安装typescript
- 进入命令行
- 输入npm i -g typescript
4.创建一个ts文件
5.使用tsc对ts文件进行编译
- 进入命令行
- 进入ts文件所在目录
- 执行命令:tsc xxx.ts
2.ts的类型声明
非函数中的变量声明
语法:
bash
let 变量名:变量类型=值
如果变量的声明和赋值是同时进行的,TS可以自动对变量进行类型检测。
函数中的类型声明
语法:
arduino
function fn(a:数据类型,b:数据类型,......):返回值的数据类型{
//函数操作
}
3.数据类型
类型 | 例子 | 描述 |
---|---|---|
number | 1,-33,2,5 | 任意数字 |
string | 'hi','hello' | 任意字符串 |
boolean | true,false | 布尔值true或false |
字面量 | 其本身 | 限制变量的值就是该字面量的值 |
any | * | 任意类型 |
unknown | * | 类型安全的any |
void | 空值(undefined) | 没有值(或undefined) |
never | 没有值 | 不能是任何值 |
object | {title:'黄昏下的蓝玫瑰'} | 任意的JS对象 |
array | [1,2,3] | 任意的JS数组 |
tuple | [4,5] | 元素,TS新增类型,固定长度数组 |
enum | enum{A,B} | 枚举,TS中新增类型 |
- 使用字面量进行类型声明
typescript
let a:10;
a=10;
a=11;//报错
let b:"male" | "female"
//b的值只能为male或female之一
- 联合类型
typescript
let c:boolean | string
c=true
c='黄昏下的蓝玫瑰'
- any 表示是任何数据类型,一个变量设置为any后相当于对该变量关闭了ts的类型检测
typescript
let d:any;
d=1;
d=true;
d='黄昏下的蓝玫瑰'
//声明变量如果不指定类型,则TS解析器会自动判断变量的类型为any(隐式的any)
let d;
d=1;
d=true;
d='黄昏下的蓝玫瑰'
- unknown 未知数据类型
typescript
let e:unknown
e=1;
e=true;
e='黄昏下的蓝玫瑰'
- any和unknown的区别
typescript
let s:string;
//不报错,d的类型为any,它可以赋值给任意变量,导致该变量也被迫关闭类型检测
s=d;
//报错
s=e;
//unknown类型的变量,不能直接赋值给其他变量
//解决方案:
//1.进行类型检测
if(typeof e === 'string'){
s=e
}
//2.类型断言
//告诉解析器变量的实际类型
s=e as string || s=<string>e
语法:
变量 as 数据类型
<数据类型>变量
- void 一般用于函数,用来表示空,表示函数没有返回值
- never 一般用于函数,表示永远不会返回结果
typescript
function fn2():never{
throw new Error('error')
}
- object
typescript
//对属性类型进行限制
let b:{name:string,age?:number}
//在属性名后加?表示属性可选
b={title:'黄昏下的蓝玫瑰'}
//当我们需要对对象的属性进行动态添加时,不可能每次都是加上?,这时我们可以采取以下的方式进行声明
let c:{name:string,[propName:string]:any};
//意思就是:对象里必须有name属性,其他属性可有可无。
- 对函数结构进行限制
typescript
//语法:(形参:数据类型,形参:数据类型,......)=>返回值数据类型
let d:(a:number,b:number)=>number
d=function(n1,n2){
return n1+n2
}
- array
typescript
/*
数组的类型声明
类型[] || Array<类型>
*/
let arr1:string[]
arr1=['hello','world']
let arr2:Array<number>
arr2=[1,2,3,4]
- tuple 元组,固定长度的数组
typescript
let h:[string,string];
//表示,一个元组里面只有两个数据,两个数据的数据类型都为string
- enum 枚举
typescript
enum Gender{
male,
female
}
let i:{name:string,gender:Gender};
i={
name:'陈路周',
gender:Gender.male
}
console.log(i.gender===Gender.male)
typescript
&:同时
let j:string & number
let j:{name:string} & {age:number}
j={name:'陈路周',age:20}
- 类型别名
typescript
//给string起了个别名
type myType=string
type myType=1|2|3|4|5
let k:myType
3.编译选项
打开监视模式
txt
tsc 文件名.ts -w
//但这种方式,只能同时监听一个文件
//解决方案:创建tsconfig.json
tsc -w
//同时监听多个ts文件
tsconfig.json中的配置项
虽然说利用脚手架初始化一个项目时,会自动帮我们添加一些配置,但为了更好的开发,也为了更好的排错,对于ts的一些常用配置还是要了解清楚的。
顶层配置项:include、exclude、files、compilerOptions
- include:指定哪些ts文件需要被编译
txt
*表示任意文件,**表示任意目录
- exclude:指定不需要被编译的文件
- files:指定被编译的文件(数组格式,一个一个文件名)
- compilerOptions:编译器选项
- target:指定ts被编译为的ES的版本
- module:指定使用的模块化规范
- lib:指定项目中需要使用的库
- outDir:指定编译后的文件放置的位置
- outFile:将代码合并为一个文件,设置outFile后,所有的全局作用域中的代码会被合并到同一个文件中
- allowJs:是否对js文件进行编译,默认不编译
- checkJs:检查js代码是否符合语法规范,默认是false(不检查)
- removeComments:是否移除注释
- noEmit:不生成编译后的文件
- noEmitOnError:当有错误时不生成编译后的文件
- alwaysStrict:用来设置编译后的文件是否使用严格模式
- noImplicitAny:不允许使用隐式的any类型
- noImplicitThis:不允许不明确类型的this
- strictNullChecks:严格检查空值
- strict:所有严格检查的总开关 建议设置为true
4.使用webpack打包ts文件
通常情况下,实际的开发中我们都需要使用构建工具对代码进行打包,TS同样也可以结合构建工具一起使用,下边以webpack为例介绍以下如何结合构建工具使用TS。
基础用法
-
步骤
1.初始化项目
- 进入项目根目录,执行命令
npm init -y
- 主要作用:创建package.json文件
2.下载构建工具
- npm i -D webpack-cli webpack typescript ts-loader
3.在项目根目录下创建webpack.config.js文件
- 在webpack.config.js文件中添加配置项
- entry:指定入口文件
- output
- path:指定打包文件的目录
path.resolve(__dirname,'dist')
- filename:打包后的文件
bundle.js
- path:指定打包文件的目录
- module:模块
- rules:打包规则
- loader的执行顺序为:普通loader执行的顺序是
从右向左,从下往上
- test:指定需要使用use中模块进行处理的文件
/\.ts$/
- use:指定处理模块
ts-loader
- exclude:指定不需要进行处理的文件
/node-modules/
- loader的执行顺序为:普通loader执行的顺序是
- rules:打包规则
4.在package.json文件中的
scripts
配置项中加入"build": "webpack"
5.执行npm run build进行打包
- 进入项目根目录,执行命令
优化配置
html-webpack-plugin
html-webpack-plugin的主要作用就是在webpack构建后生成html文件,同时把构建好入口js文件引入到生成的html文件中。 配置方法如下:
json
//先在顶层引入html-webpack-plugin
const HtmlWebpackPlugin=require('html-webpack-plugin')
//在module.exports={}顶层添加配置信息
module.exports={
plugins:[
new HtmlWebpackPlugin({
//模版路径
template:'./src/index.html'
})
]
}
clean-webpack-plugin
在每次打包发布时自动清理掉 dist 目录中的旧文件
json
//先在顶层引入插件
const {CleanWebpackPlugin}=require('clean-webpack-plugin')
//在module.exports={}顶层添加配置信息
module.export={
plugins:[
new CleanWebpackPlugin()
]
}
使用引用模块时报错
报错信息:D:\WPSTest\StydyTest\ts\src\m1 doesn't exist 解决方案如下:
json
//在module.exports={}顶层添加配置信息
module.export={
//声明以js和ts结尾的文件可以被引用
resolve:{
extensions:['.ts','.js']
}
}
解决代码在浏览器兼容的问题
一些低版本的浏览器可能不支持es6的新语法,这是我们就需要借助babel来解决兼容问题
1.安装插件
bash
npm i -D @babel/core @babel/preset-env babel-loader core-js
2.添加配置信息
json
rules:[
{
test:/\.ts$/,
use:[
//配置babel
{
//指定加载器
loader:"babel-loader",
//设置babel
options:{
//设置预定义的环境
presets:[
[
//指定环境插件
" @babel/preset-env",
//配置信息
{
//要兼容的目标浏览器
targets:{
//指定浏览器版本
"chroms":"88"
},
//指定corejs的版本
"corejs":"3",
//使用corejs的方式 "usage"表示按需加载
useBuiltIns:"usage"
}
]
]
}
}
'ts-loader'
]
}
]
后续我们只要将想要兼容的浏览器版本添加到targets中就可以了
environment
告诉webpack不适用箭头函数(添加这个配置项是因为当时想兼容不支持ES6语法的浏览器,比如IE)
json
//在顶层module.export={}加入以下配置信息
environment:{
arrowFunction:false
}
5.面向对象
- 书写格式
typescript
class Dog{
name:string;
age:number
constructor(name:string,age:number){
this.name=name
this.age=age
}
//定义在实例上的方法
sayHello(){
console.log('汪汪汪')
}
}
- 继承
- 可以在不修改原来类的基础上,添加新的功能。
- 实现:定义一个新类,继承旧类,将需要拓展的新方法和属性定义在新类中
- 方法重写:如果在子类中添加了和父类相同的方法,则子类方法会覆盖掉父类的方法
typescript
class Animal{
name:string;
age:number;
constructor(name:string,age:number){
this.name=name
this.age=age
}
sayHello(){
console.log('汪汪汪')
}
}
class Dog extends Animal{
run(){
console.log(this.name+'is keeping running')
}
}
class Cat extends Animal{
}
let cat=new Cat('咪咪',3)
let dog=new Dog('旺旺',5)
dog.run()
- super
应用场景:拓展新属性时
typescript
class Cat extends Animal{
gender:number;
constructor(name:string,age:number,gender:number){
super(name,age)
this.gender=gender
}
}
let cat=new Cat('咪咪',3,1)
let dog=new Dog('旺旺',5)
dog.run()
console.log(cat)
- 抽象类
- 以abstract开头的类是抽象类
- 抽象类和其他类的区别不大,只是不能用来创建对象
- 抽象类就是专门用来被继承的
- 抽象方法
- 抽象类中可以添加抽象方法(以abstract开头)
- 抽象方法只能定义在抽象类中,且子类必须对抽象方法进行重写
typescript
abstract class Animal{
name:string;
age:number;
constructor(name:string,age:number){
this.name=name
this.age=age
}
abstract sayHello():void
}
class Dog extends Animal{
sayHello(): void {
console.log('汪汪汪')
}
run(){
console.log(this.name+'is keeping running')
}
}
class Cat extends Animal{
gender:number;
constructor(name:string,age:number,gender:number){
super(name,age)
this.gender=gender
}
sayHello(): void {
console.log('汪汪汪')
}
}
let cat=new Cat('咪咪',3,1)
let dog=new Dog('旺旺',5)
dog.run()
console.log(cat)
- 接口
- 用来定义一个类中应该包含哪些属性和方法
- 接口中的所有属性都不能实际值
- 接口只定义对象的结构,而不考虑实际值
- 在接口中的所有方法都是抽象方法
typescript
interface myInterfaceType {
name: string
age: number
}
const obj: myInterfaceType = {
name: '遇萤',
age: 22
}
console.log(obj)
interface newType {
name: string;
sayHello(): void
}
- 属性封装:属性可以被任意的修改导致对象的数据变得非常的不安全
- 属性修饰符
- private:通过private定义的属性可以通过在类中添加方法使得外部访问到private属性
- public
- protected:受保护的属性,只能在当前类和当前类的子类中使用
- getter和setter
- get 属性(){return this.属性} set 属性(val:属性值类型){修改操作}
- 属性修饰符
typescript
class MyClass implements newType {
name: string;
constructor(name: string) {
this.name = name
}
sayHello(): void {
console.log('qqqq')
}
}
class Person{
private _name:string;
private _age:number;
constructor(name:string,age:number){
this._name=name
this._age=age
}
sayHello(){
console.log('hi~')
}
// 定义方法,用来获取name属性
getName(){
return this._name
}
setName(val:string){
this._name=val
}
}
let per1=new Person('遇萤',20)
- 泛型
- 在定义函数或是类时,如果遇到类型不明确就可以使用泛型
javascript
function fn<T>(a:T):T{
return a
}
// 可以直接调用具有泛型的函数
let res=fn(10); //不指定泛型,TS可以自动对类型进行推断
let res2=fn<string>('遇萤') //指定泛型
// 泛型可以同时指定多个
function fn2<T,K>(a:T,b:K):T{
console.log(b)
return a
}
fn2<number,string>(20,'遇萤')
//限制泛型的范围
interface Inter{
length:number;
}
// T extends Inter 表示泛型T必须是Inter实现类(子类)
function fn3<T extends Inter>(a:T):number{
return a.length
}
console.log(fn3({length:123}))
class MyClass<T>{
name:T;
constructor(name:T){
this.name=name
}
}
const mc=new MyClass<string>('遇萤')
遇到的问题
1.typescript | 解决tsconfig.json提示"ts找不到全局类型Array、Boolean"、
报错原因:lib数组里的项存在依赖关系
解决方案:加入"esnext"项就好了。
json
{
"compilerOptions": {
"allowJs": true,
"target": "es5",
"lib": [
"dom",
"esnext",
"es2015.promise"
]
}
}
2.关于使用webpack热更新出现 cannot get/chrome.exe
原因不明
解决方案:
json
//原始写法
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack --mode production",
"dev": "webpack -- mode development",
"start": "webpack serve --open chrome.exe"
},
//正确写法 去掉chrome.exe
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack --mode production",
"dev": "webpack -- mode development",
"start": "webpack serve --open"
},
上面记录的都是作者自学时记录的笔记,比较基础。因为之前学的是js,完全没接触过ts,上面的内容只是帮助自己先入个门,这里推荐大家去看尚硅谷李立超老师讲的typescript,老师特别有梗!真的!!!!简单入门之后,一定要对所学知识进行检测。跟着李老师的课学,后面会讲一个实践(贪吃蛇小游戏)。
创建基于 TS 的 React 项目
实习最后一次考核的内容是搭建一个后台管理系统,只有两个模块,数据模块和标签模块。数据模块包含增删改查以及分页五个功能,标签模块包含增删改查四个功能。
前端搭建
1.项目初始化
lua
npx create-react-app my-app --template typescript
2.相关依赖下载
bash
# 仓库相关插件下载
npm i redux @reduxjs/toolkit react-redux
# 路由相关
npm i react-router-dom
# 框架相关
npm i antd
# 请求相关
npm i axios
别名配置
1.新建一个config-overrides.js文件,添加以下代码
js
const { override, addWebpackAlias } = require('customize-cra')
const path = require('path')
module.exports = override(
addWebpackAlias({
// 指定@符指向src目录
'@': path.resolve(__dirname, 'src'),
})
)
2.检查tsconfig.json文件中paths字段是否存在对应配置,如不存在需要加上
json
"paths":{
"@/*":["./src/*"],
}
3.重新启动项目后就可以使用对应别名了
如何修改webpack配置
选择react框架进行前端开发时,基本上都会用到create-react-app
这个官方提供的脚手架。但是这个脚手架有个很大的弊端,不能直接对该脚手架的默认选项进行配置。相比于vue最新的脚手架 @vue/cli而言,@vue/cli虽然也对webpack的配置进行了全面的封装,但是官方允许用户在项目根目录创建一个vue.config.js进行配置,并且提供了丰富的API,供用户去参考。如果用户想配置create-react-app
脚手架中的webpack进行方便开发的话,似乎之后通过npm run eject这一条路走,网上大部分的教程也是这样的,即把默认配置全部弹出进行操作。但是这种方法有两个缺点:npm run eject
命名不可逆,一旦配置文件暴露后就不可再隐藏;扩展的配置和create-react-app内建的webpack配置混合在了一起,不利于配置出现问题后的排查。 利用craco进行在配置 1.插件下载
css
npm i -D @craco/craco
2.在项目的根目录下新建craco.config.js
文件,后续的webpack配置写在这里。
3.修改package.json
里的启动配置
json
"scripts": {
"client": "craco start",
"server": "node ../server/app",
"start": "npm run client --mode dev",
"build": "craco build --mode production",
"test": "craco test",
"eject": "react-scripts eject"
}
配置代理
方法一:利用http-proxy-middleware
1.依赖下载
css
npm i -D http-proxy-middleware
2.在项目根目录下新建一个setupProxy.js文件,并添加配置信息
js
const {createProxyMiddleware}=require('http-proxy-middleware')
module.exports=function(app:any){
app.use(
createProxyMiddleware(
'/api',//代理名称
{
target:'http://localhost:8081',
changeOrigin:true,
pathRewrite:{"^/api":" "} //请求中将/api替换成空
}
),
)
}
方法二:使用webpack-dev-server的proxy配置,在craco.config.js
里添加配置信息。
javascript
const CracoLessPlugin = require("craco-less")
const path = require('path')
const pathResolve = pathUrl => path.join(__dirname, pathUrl)
module.exports = {
//代理
devServer: {
proxy: {
'/api': {
target: 'http://localhost:3001',
changeOrigin: true,
pathRewrite: {
"^/api": ''
}
}
},
historyApiFallback:true,
},
babel: {
plugins: [
['@babel/plugin-proposal-decorators', {
legacy: true
}]
]
},
plugins: [{
plugin: CracoLessPlugin,
options: {
lessLoaderOptions: {
lessOptions: {
modifyVars: {},
javascriptEnabled: true
}
}
}
}]
}
样式配置
css
npm i -D sass
新建对应的scss文件,采用less的格式书写样式,在需要使用样式的组件中引入scss文件
仓库配置
一个有用的插件,可看到更新后的state Redux DevTools(直接在扩展商城搜)
1.store/index.ts
typescript
import { configureStore } from "@reduxjs/toolkit";
import logger from "redux-logger";
import { useDispatch, useSelector, TypedUseSelectorHook } from "react-redux";
import users from "./modules/users";
// 配置中间件
// RTk已经默认使用了redux-thunk,这里不需要额外引入了
// 如果需要一些自定义的中间件,可以通过调用getDefaultMiddleware
// 并将结果包含在返回的中间件数组中
// 案例中使用了日志的中间件,可以追踪到哪个页面在哪个时候使用了该reducer
// 并且可以显示调用前的数据状态和调用后的数据状态middleware: (getDefaultMiddleware) => getDefaultMiddleware({}).concat(logger),
export const store = configureStore({
reducer: { users },
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({ serializableCheck: false }).concat(logger),
devTools: process.env.NODE_ENV !== "production",
});
// 全局定义 dispatch和state的类型,并导出
// 后面使用过程中直接从该文件中引入,而不需要冲react-redux包中引入
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
2.子仓库配置
我这里是把子仓库放置在store/modules文件夹下
store/modules/user.ts
ts
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
interface IUser {
userName: String,
userPass: String,
userNick: String,
userImg: String,
userEmail: String,
userPhone: String,
_id: string,
token: string
}
const initialState = {
userName: 'admin',
userPass: '20020915',
userNick: '',
userImg: '/img/a.jpg',
userEmail: '',
_id: '',
userPhone: '',
token: ''
} as IUser;
const counterSlice = createSlice({
name:'logininit', //仓库名称
initialState:initialState,
reducers:{
Init:(state:IUser,action:any)=>{
console.log(action)
state={...action.payload}
return state
},
}
})
export const {Init}=counterSlice.actions
export default counterSlice.reducer
3.项目的入口文件index.tsx中挂载仓库
4.在组件中使用dispatch和获取仓库数据
tsx
# 引入操作方法
import { useAppDispatch, useAppSelector } from '../../store/index'
# 获取仓库数据
const userMes = useAppSelector((state) => state.users);
# 操作仓库
const dispatch = useAppDispatch()
dispatch({
type: "datainit/Init",
payload: [...res.all]
})
redux获取数据存在bug
当即将需要进行渲染的组件需要获取相应的数据时,首先想到的是通过路由传递数据或者通过redux获取数据。但这两种方式都存在数据丢失的风险,对于路由传递数据,只要刷新页面,数据就有可能丢失(路由数据必须由上一级传递到下一级才能成功获取)。对于redux,由于初始数据是我们在登陆成功跳转前利用dispatch写入的,只要一刷新,仓库的state就会恢复到初始状态。这里提供一种种方式,通过路由中的loader配置项,实现刷新就会重新获取而不会导致数据丢失的功能。我们只需要在对应路由组件中通过useLoaderData进行获取即可。其实也可以利用useRef对初始数据进行拷贝。
tsx
{
path: 'person',
element: <Suspense><Person /></Suspense>,
loader: async () => {
let str = localStorage.getItem('ECSDVEFT_SXFSC') as string
return await getUser({ token: str })
},
}
路由配置
由于路由配置与我之前js版本的配置没有区别,这里就不再记录。现在来说一下,在使用router时遇到的问题。
1.权限控制
在需要登录的系统中,如果未登录前不允许通过在地址栏输入对应路由地址从而实现跳转。在vue中可以通过路由守卫实现此限制,可在react中没有路由路由守卫这种说法。我查看了很多文档,最多的解决办法都是封装一个权限控制组件,从而实现路由权限控制。我看的头都大,又倒回去看v6版本路由的教程,在看到loader这个字段的时候,我就想是不是可以利用loader和redirect结合,从而实现此功能呢。我测试了一下,是可以的。事实证明,多看文档是会发现惊喜的。如果大家还有更好的解决办法,可以share一下呀!大家互相学习嘿嘿。
[router:文档链接](Home v6.15.0 | React Router)
这个是另外一个后台管理的项目的路由表
typescript
import { createBrowserRouter } from 'react-router-dom'
import React, { Suspense, useRef } from "react"
import Login from '@/views/Login/Login'
import NotFound from '@/component/NotFind/NotFind'
import { judgeToken } from '@/api/list'
import { redirect, useNavigate } from "react-router-dom";
import { message } from 'antd';
import { getUser } from '@/api/list'
const Home = React.lazy(() => import('@/views/Home/Home'))
const Person = React.lazy(() => import('@/views/Person/Person'))
// 未登录前不允许通过地址栏跳转至对应的页面
const loader = async () => {
const user = localStorage.getItem('ECSDVEFT_SXFSC')
if (!user) {
console.log(user)
message.warning('请先登录!')
return redirect("/");
}
let str = localStorage.getItem('ECSDVEFT_SXFSC') as string
return await getUser({ token: str })
};
export default createBrowserRouter([
{
path: '/',
element: <Login />,
},
{
path: '/home',
loader: loader,
element: <Suspense><Home /></Suspense>,
children: [
{
path: 'person',
element: <Suspense><Person /></Suspense>,
loader: async () => {
let str = localStorage.getItem('ECSDVEFT_SXFSC') as string
return await getUser({ token: str })
},
},
{
// 不匹配时进入
path: '*',
element: <NotFound></NotFound>
}
]
},
{
// 不匹配时进入
path: '/404',
element: <NotFound></NotFound>
}
])
2.按用户权限进行页面渲染
问题描述:在后台管理这个系统中,有很多的用户,每个用户的权限都是不一样的。比如,当用户未超级管理员时,他拥有最高权限,可以查看并操作系统中的任何数据。当用户为普通用户时,他只能进行对自己信息的修改以及一些上层创建他的管理员赋予他的权限,当他通过地址栏访问了无权访问的页面,要报404,而不是直接进入页面。
解决思路:我最开始还是想用loader去解决,但矛盾这时产生了,如果是通过点击跳转到对应页面,那么通过window.location.pathname
来获取当前路径获取的是跳转前的路径。但如果是通过在地址栏输入进行跳转,是可以使用loader
和window.location.pathname
结合进行限制的。因此,我们还需要在组件中进行现在,在对应的跳转函数中进行用户权限的判断,没有权限则跳转到404组件。
typescript
// 没有权限不允许通过地址栏跳转至对应的页面
const judge = async () => {
let str = localStorage.getItem('ECSDVEFT_SXFSC') as string
let res = await getUser({ token: str })
let currentPath = window.location.pathname
let powerArr = res.data.loginRole.rolePower
if (powerArr.includes(currentPath)) {
//有权限
return powerArr
} else {
// 无权限 跳转404
return redirect("/404")
}
};
//在操作函数中进行限制
// 顶部菜单跳转路由
const select = (item) => {
//let powerArr=data.data.loginRole.rolePower
if(data.includes(item.key)){
navigate(item.key)
}else{
navigate('/404')
}
}
总结:其实我想实现的是,根据用户的权限去渲染组件,而不是把所有的功能都展示出来,点击跳转之后再去判断用户有没有权限,如果大家有好的解决方法,可以分享出来大家一起学习学习呀!
axios封装
项目运行npm run build
打包之后,把build
作为后端的静态文件夹资源,使用localhost:3001
打开项目后,页面报404跨域了!我还纳闷了很久,明明已经配置了代理,为什么还会跨域。我们需要分清楚,代理是我们配置在开发环境中的,在生产环境自然会失效。这是怎么办呢?我搜索了很多文章,网上的解决方法主要是配置nginx,这玩意我配不明白,我就放弃了,选择另外一种方式。
解决办法: 在axios发送请求前,先判断此时是生产环境还是开发环境
开发环境下,访问的是/api/user/getdata
,利用代理解决跨域问题
生产环境下,由于我们利用koa-static将静态文件夹定位到打包后build文件夹,所以请求的是由后端直接发起的因此,不存在跨域问题。此时请求的地址为:http://localhost:3001/user/getdata
arduino
const service: AxiosInstance = axios.create({
timeout,
baseURL:process.env.NODE_ENV=='development' ? '/api' : 'http://localhost:3001',
headers: {
Accept: 'application/json',
'X-Requested-With': 'XMLHttpRequest',
},
// 如需要携带cookie 该值需设为true
withCredentials: true
});
//baseURL,添加公共前缀
export const GetData = (params: any) => request.get<any>('/user/getdata', params, { timeout: 15000 });
配置package.json
json
//打包的时候,声明此时是生产环境
//npm start时,声明此时是开发环境
"start": "craco start --mode dev",
"build": "craco build --mode production",
封装好的axios也分享给大家
typescript
import axios from "axios";
import type { AxiosInstance, AxiosError, AxiosRequestConfig } from 'axios'
import { message } from 'antd';
// 基础URL,axios将会自动拼接在url前
let baseURL = process.env.NODE_ENV;
const getAuthToken = (token: string) => `Bearer ${token}`;
// 默认请求超时时间
const timeout = 30000;
// 创建axios实例
const service: AxiosInstance = axios.create({
timeout,
baseURL:process.env.NODE_ENV=='development' ? '/api' : 'http://localhost:3001',
headers: {
Accept: 'application/json',
'X-Requested-With': 'XMLHttpRequest',
},
// 如需要携带cookie 该值需设为true
withCredentials: true
});
// 统一请求拦截 可配置自定义headers 例如 languange、token等
service.interceptors.request.use(
(config) => {
// 配置自定义请求头
// let customHeaders: AxiosRequestHeaders = {
// language:'zh-cn',
// };
// config.headers = customHeaders;
// let token = localStorage.getItem('ECSDVEFT_SXFSC')!
// if(!token){
// config.headers['authorization'] = getAuthToken(token);
// }
let token = localStorage.getItem("ECSDVEFT_SXFSC")
//token存在就赋值 不存在不执行
token && (config.headers['Authorization'] = token)
// console.log('token存在')
//console.log(config);
return config
},
(error: AxiosError) => {
//console.log(error);
Promise.reject(error)
}
)
// axios 返回格式
interface axiosTypes<T> {
data: T;
status: number;
statusText: string;
}
//核心处理代码 将返回一个promise 调用then将可获取响应的业务数据
const requestHandler = <T>(method: 'get' | 'post' | 'put' | 'delete', url: string, params: object = {}, config: AxiosRequestConfig = {}): Promise<T> => {
let response: Promise<axiosTypes<any>>;
switch (method) {
case 'get':
response = service.get(url, { params: { ...params }, ...config });
break;
case 'post':
response = service.post(url, { ...params }, { ...config });
break;
case 'put':
response = service.put(url, { ...params }, { ...config });
break;
case 'delete':
response = service.delete(url, { params: { ...params }, ...config });
break;
}
return new Promise<T>((resolve, reject) => {
response.then(({ data }) => {
//业务代码 可根据需求自行处理
//console.log(data.errCode)
if (data.errCode !== 0) {
reject(data);
} else {
//数据请求正确 使用resolve将结果返回
//console.log(data)
if ('showMsg' in data) {
if (data.showMsg) {
if (data.flag) {
message.success(data.showMsg);
//message.success(data.msg as string)
} else if (!data.flag) {
message.warning('' + data.showMsg)
}
}
}
//message.success(data.msg)
resolve(data);
}
}).catch(error => {
// let e = JSON.stringify(error);
message.warning(`网络错误:${error}`);
// console.log(`网络错误:${e}`)
//console.log(`网络错误:${error}`)
reject(error);
})
})
}
// 使用 request 统一调用,包括封装的get、post、put、delete等方法
const request = {
get: <T>(url: string, params?: object, config?: AxiosRequestConfig) => requestHandler<T>('get', url, params, config),
post: <T>(url: string, params?: object, config?: AxiosRequestConfig) => requestHandler<T>('post', url, params, config),
put: <T>(url: string, params?: object, config?: AxiosRequestConfig) => requestHandler<T>('put', url, params, config),
delete: <T>(url: string, params?: object, config?: AxiosRequestConfig) => requestHandler<T>('delete', url, params, config)
};
// 导出至外层,方便统一使用
export { request };
后端搭建
之前学习node.js时,使用的是express框架搭配mongoose使用,实习阶段,公司要求我们使用koa来搭建接口,使用lowdb来存储数据。
依赖下载:
- koa
- koa-static
- koa-router
- koa-bodyparser
- lowdb
- koa2-connect-history-api-fallback
koa-static配置 使用方法很简单,只需要在入口文件配置以下语句就ok啦~
javascript
app.use(static('../client/build'))
koa-router和lowdb配置 1.新建route文件夹 2.新建父路由文件index.js
,配置以下信息
javascript
const Router = require('koa-router');
const user = require('./user');
const router = new Router();
// 指定一个url匹配
router.get('/', async (ctx) => {
ctx.type = 'html';
ctx.body = '<h1>hello world!</h1>';
})
router.use('/user', user.routes(), user.allowedMethods());
module.exports = router;
3.新建子路由文件user.js
javascript
const Router = require('koa-router');
const router = new Router();
const {getTime}=require('../handler/options')
// const {db}=require('../handler/db')
const low = require('lowdb');
const FileSync = require('lowdb/adapters/FileSync'); // 有多种适配器可选择
const adapter = new FileSync('../server/db.json'); // 申明一个适配器
const db = low(adapter);
const {RangeTime,IncludeTabs,IncludeTime,Used,UsedTabs} =require('../handler/options')
const dayjs=require('dayjs')
router.get("/", async (ctx) => {
console.log('查询参数', ctx.query);
ctx.body = '获取用户列表';
})
.get("/getdata", async (ctx) => {
})
.post("/language", async (ctx) => {
})
.get("/getlanguage",async(ctx)=>{
})
koa-bodyparser配置 若想获取post请求的携带的参数,还需要配置koa-bodyparser
在入口文件中添加一下配置信息
javascript
app.use(bodyPramas());
app.use(router.routes());
app.use(router.allowedMethods());
总结:后端使用了好几个中间件,使用的时候一定要注意他们的使用顺序,像koa2-connect-history-api-fallback
就必须放置在静态文件夹之前,才能解决我们的问题,至于koa2-connect-history-api-fallback
作用是什么,后文会解释的。
在上面所有的配置完成之后,只要我们的项目书写完毕,打包之后,运行后端,再去访问后端地址是可以成功访问到我们的项目的,但是,这时又冒出来一个bug
,哦豁,只要页面刷新,就会报404。这又是为何呢?这里就不得不提一下history和hash两种路由模式的区别啦。
history和hash的区别
hash
:即地址栏URL中的#
符号。比如这个URL:http://www.abc.com/#/hello
,hash
的值为#/hello
。它的特点在于:hash虽然出现在URL中,但不会被包括在HTTP请求中,对后端完全没有影响,因此改变hash不会重新加载页面。
history
:利用了HTML5 History Interface
中新增的pushState()
和replaceState()
方法(需要特定浏览器支持)。这两个方法应用于浏览器的历史记录栈,在当前已有的back、forward、go
的基础之上,它们提供了对历史记录进行修改的功能。只是当它们执行修改时,虽然改变了当前的 URL, 但浏览器不会立即向后端发送请求。
history模式开发的SPA项目,需要服务器端做额外的配置,否则会出现刷新白屏(链接分享失效)。原因是页面刷新时,浏览器会向服务器真的发出对这个地址的请求,而这个文件资源又不存在,所以就报404。处理方式就由后端做一个保底映射:所有的请求全部拦截到index.html上。
解决办法:
方法一:使用hash模式的路由(经测试,有效)
方法二:配置nginx(但这种方法好像不生效,这个nginx tmmd一直配不明白)
coffeescript
server {
listen 80;
server_name localhost;
location / {
root html;
index index.html;
try_files $uri $uri/ /index.html;
}
}
方法三:后端进行处理,使用koa2-connect-history-api-fallback
中间件
1.插件下载
arduino
npm i -D koa2-connect-history-api-fallback
2.在入口文件中进行配置
js
const { historyApiFallback } = require('koa2-connect-history-api-fallback');
const app = new Koa(); // 创建koa应用
const static=require('koa-static') //配置静态文件夹
// koa2-connect-history-api-fallback中间件一定要放在静态资源服务中间件前面加载
app.use(historyApiFallback({index: '/index.html'}))
app.use(static('../client/build'))
总结
到这里,所有的内容就记录完了。文章通篇都还是在讲述怎么配置各种插件和工具怎么配置怎么使用,分享出来,除了为了回顾并记录自己这一个半月的学习过程,还有一个目的是希望看到我文章的宝子在遇到同样的坑时,不至于挠破头皮也找不到解决办法。解决bug的过程是痛苦的,有时候一天下来就解决了一个bug,不要觉得这时效率不高的表现!真的就是在报错中进步呀!!!