0%

简介

  • 项目总结代码地址
  • 记录开发 react-native 中各种有意思的事情
  • 最新探索,用来进行各种尝试的空间
  • 完成的探索,记录所有已经完成的模块、功能和效果
  • 中转站,暂时无用

安装

全局脚手架

1
npm i -g react-native-cli

安装基础项目

1
react-native init <项目名称>

运行

优化后的运行

  • ios:npm run ios
  • android:npm run android
  • ios and android:npm run s

原始启动

  • ios:react-native run-ios
  • android:react-native run-android

资源

未探索的资源

365 个库中寻找 77 个

新增页面需要在三个地方进行操作

  1. data 里面的 complete.js 中添加数据,方便在完成的探索中出现
  2. navigators 中的 AppStackNavigators.js 里面进行引入=>注册,使得路由正常
  3. pages 里面添加页面

优化 package.json 使开发更加方便

  • 在 scripts 中添加,如下内容:
1
2
3
"ios":"react-native run-ios",
"android":"react-native run-android",
"s":"npm run ios && npm run android",

flex 布局常用属性

  • 防止在 flex 中出现,固定元素被挤压的问题,使用 flex:1 进行优化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
display: 'flex';
flexDirection: row | row-reverse | column | column-reverse;
flexWrap: nowrap | wrap | wrap-reverse;
flexFlow: <flex-direction> | <flex-wrap>;
justifyContent: flex-start | flex-end | center | space-between | space-around;
alignItems: flex-start | flex-end | center | baseline | stretch;
alignContent: flex-start | flex-end | center | space-between | space-around | stretch;

order: <integer>;
flexGrow: <number>; /* default 0 */
flexShrink: <number>; /* default 1 */
flexBasis: <length> | auto; /* default auto */
flex: none | [ <'flex-grow'> <'flex-shrink'>? || <'flex-basis'> ]
alignSelf: auto | flex-start | flex-end | center | baseline | stretch;

图片

图片使用

1
<Image source={require('./my-icon.png')} />

命名规则

链接原生库

第一步

安装带原生依赖的库

1
npm install <某个带有原生依赖的库> --save

第二步

运行以下命令,它会根据 package.json 文件中的 dependencies 和 devDependencies 记录来链接所有需要链接的库

1
react-native link

开发技巧

  • 启动 hot 或者 debug 的时候,切记不要同时启动 ios and android,两者会发生冲突,导致出现程序崩溃,从而影响开发
  • 最好的方式是,启动一个 hot 或 debug,这样可以良好的进行开发;另一个手动足够了

处理触摸事件

使用”Touchable”开头的一系列组件
通过 onPress 属性接受一个点击事件的处理函数

  • 两种方式绑定事件,并处理点击范围
  • 推荐第二种,方便传递数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
_onPressButton=((item)=>{
alert(1)
console.log(item);
})
render() {
return (
<View>
<TouchableHighlight onPress={this._onPressButton}>
<View style={[S.btn,{backgroundColor:`${this.state.bg}`}]}>
<Text style={S.btnText}>{this.state.bg}</Text>
</View>
</TouchableHighlight>
<TouchableHighlight onPress={()=>{
this._onPressButton(this.state.bg)
}}>
<View style={[S.btn,{backgroundColor:`${this.state.bg}`}]}>
<Text style={S.btnText}>{this.state.bg}</Text>
</View>
</TouchableHighlight>
</View>
);
}

解决版本冲突问题

js 版本和 rn 版本出现冲突

  • Watchman 是由 Facebook 提供的监视文件系统变更的工具。安装此工具可以提高开发时的性能(packager 可以快速捕捉文件的变化从而实现实时刷新)。
  • 清除 Watchman:watchman watch-del-all
  • 清除 rn 缓存:react-native start –reset-cache

解决路由失效问题

  • createTabNavigator 要在 createStackNavigator 里面进行注册,才能够使用 createStackNavigator 具有的方法,这个是我第二次配置的时候掉进的一个坑
    this.props.navigation 属性详细说明

  • 在 createTabNavigator 里面没有 push、replace 等方法

  • 在 createStackNavigator 中存在 push、replace 等方法

AsyncStorage 保存报错

JSON value ‘‘ of type NSNull

  • 当保存值为 undefined 的时候,报错
  • 防止这种问题发生最好在 this.state 中保存初始值为空,必要时可以提醒用户

react-native 支持的 style props

  • 查看最新版属性支持情况,最简单的直接在页面使用错误的属性,然后查看报错信息 😂
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
Valid style props: [
"alignContent",
"alignItems",
"alignSelf",
"aspectRatio",
"backfaceVisibility",
"backgroundColor",
"borderBottomColor",
"borderBottomEndRadius",
"borderBottomLeftRadius",
"borderBottomRightRadius",
"borderBottomStartRadius",
"borderBottomWidth",
"borderColor",
"borderEndColor",
"borderEndWidth",
"borderLeftColor",
"borderLeftWidth",
"borderRadius",
"borderRightColor",
"borderRightWidth",
"borderStartColor",
"borderStartWidth",
"borderStyle",
"borderTopColor",
"borderTopEndRadius",
"borderTopLeftRadius",
"borderTopRightRadius",
"borderTopStartRadius",
"borderTopWidth",
"borderWidth",
"bottom",
"color",
"decomposedMatrix",
"direction",
"display",
"elevation",
"end",
"flex",
"flexBasis",
"flexDirection",
"flexGrow",
"flexShrink",
"flexWrap",
"fontFamily",
"fontSize",
"fontStyle",
"fontVariant",
"fontWeight",
"height",
"includeFontPadding",
"justifyContent",
"left",
"letterSpacing",
"lineHeight",
"margin",
"marginBottom",
"marginEnd",
"marginHorizontal",
"marginLeft",
"marginRight",
"marginStart",
"marginTop",
"marginVertical",
"maxHeight",
"maxWidth",
"minHeight",
"minWidth",
"opacity",
"overflow",
"overlayColor",
"padding",
"paddingBottom",
"paddingEnd",
"paddingHorizontal",
"paddingLeft",
"paddingRight",
"paddingStart",
"paddingTop",
"paddingVertical",
"position",
"resizeMode",
"right",
"rotation",
"scaleX",
"scaleY",
"shadowColor",
"shadowOffset",
"shadowOpacity",
"shadowRadius",
"start",
"textAlign",
"textAlignVertical",
"textDecorationColor",
"textDecorationLine",
"textDecorationStyle",
"textShadowColor",
"textShadowOffset",
"textShadowRadius",
"textTransform",
"tintColor",
"top",
"transform",
"transformMatrix",
"translateX",
"translateY",
"width",
"writingDirection",
"zIndex"
]

StatusBar 解决 android 状态栏问题

设置 StatusBar

1
2
3
4
<StatusBar
backgroundColor="transparent"
translucent
/>

设置顶部模块的样式

  • 注:padding 值需要+高度值=>height 最终值
1
2
height: 50,
paddingTop: 10,

Platform 区分 ios and android 平台

三种方法实现跨平台:

  1. Platform.OS
  2. 使用 Platform 的 select 方法
  3. 使用不能组件名:ios 使用 header.ios.js;android 使用 header.android.js
1
2
3
4
const instructions = Platform.select({
ios: 'ios',
android: 'android',
});

请求 fetch 封装

  • HttpUtils.js
    引入
1
import HttpUtils from '../../HttpUtils';

使用

  • 在全局 global.data.domain 保存请求域名
  • 请求需要 header 传递 token 的使用,global.data.token
1
2
3
4
5
6
7
8
9
10
HttpUtils.post(`${apiConfig}`, {
platform: 'ios'
},global.data.token)
.then(rtn => {
global.data.config = rtn;
// console.log(`配置=>( ${JSON.stringify(rtn)} )`);
})
.catch(err => {
console.log('error', err)
})

请求环境+请求地址定义

请求环境

  • 在 addr.js 中 env 变量定义

请求地址

  • 在 addr.js 中 addrList 统一管理所有请求地址

使用

1
2
import addrList from '../../addr';
const {apiConfig}=addrList;

react-native-scrollable-tab-view

  • 解决逗号影响的报错问题:在路径~/node_modules/react-native-scrollable-tab-viewSceneComponent.js 下删除即可

react-native 初始化报错,导致无法初始化

  • 重新设置一下地址
1
2
npm config set registry https://registry.npm.taobao.org
npm config set disturl https://npm.taobao.org/dist

动态切换状态

  • 采用对象的形式来操作,这样可以使用 state 里面的参数,来动态切换
  • 解决了直接应用的报错问题,以后遇到同样的问题要及时想到对象方式

声明周期-初始渲染 and 检测 goBack()

  • 为了保证页面的数据实时性,最好在初始的时候进行请求
1
componentDidMount(){}

检测 goBack()

需要重新加载 app

第一步

  • 在删除组件的时候,定义 goBack 事件
1
2
3
componentWillUnmount(){
DeviceEventEmitter.emit('goBack', 'goBack刷新返回页面数据');
}

第二步

  • 在初始渲染中监听,goBack 自定义事件
1
2
3
4
5
6
componentDidMount(){
// 接收
this.deEmitter = DeviceEventEmitter.addListener('goBack', (a) => {
this.loadData();
});
}

安卓打包

第一步:生成 Android 签名证书

第二步:设置 gradle 变量

  • 将你的签名证书 copy 到 android/app 目录下
  • 编辑~/.gradle/gradle.properties../android/gradle.properties(一个是全局 gradle.properties,一个是项目中的 gradle.properties,大家可以根据需要进行修改) ,加入如下代码:
1
2
3
4
MYAPP_RELEASE_STORE_FILE=ch09-keystore.jks
MYAPP_RELEASE_KEY_ALIAS=ch09-keystore
MYAPP_RELEASE_STORE_PASSWORD=123456
MYAPP_RELEASE_KEY_PASSWORD=123456

第三步:在 gradle 配置文件中添加签名配置

  • 编辑android/app/build.gradle文件添加如下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
...
android {
...
defaultConfig { ... }
signingConfigs {
release {
storeFile file(MYAPP_RELEASE_STORE_FILE)
storePassword MYAPP_RELEASE_STORE_PASSWORD
keyAlias MYAPP_RELEASE_KEY_ALIAS
keyPassword MYAPP_RELEASE_KEY_PASSWORD
}
}
buildTypes {
release {
...
signingConfig signingConfigs.release
}
}
}
...

第四步:签名打包 APK

  • terminal 进入项目下的 android 目录,运行如下代码:
1
./gradlew assembleRelease
  • 签名打包成功后你会在 android/app/build/outputs/apk/目录下看到签名成功后的 app-release.apk 文件。
  • 提示:如果你需要对 apk 进行混淆打包 编辑 android/app/build.gradle:
1
2
3
4
5
/**
* Run Proguard to shrink the Java bytecode in release builds.
*/

def enableProguardInReleaseBuilds = true

错误处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
fetch(apiLogin, {
method: 'POST',
headers: {
'Accept': 'application/json',//通过头指定,获取的数据类型是JSON
'Content-Type': 'application/json',
'Authorization':global.data.token,
},
body: JSON.stringify({
// 请求参数
})
})
.then(result => {
const {status,statusText} = result;
if(status>=200 && status<300 || status==304){
// 成功
const promise = result.json()
promise.then(rtn=>{
// 成功
})
}else if(status==403){
// 跳转登录
this.props.navigation.replace('login');
}else{
// 提醒 statusText
this.refs.toast.show(statusText);
}
})
.catch(error => {
if(Object.prototype.toString.call(error) === '[object Object]'){
this.refs.toast.show(JSON.stringify(error))
}else{
this.refs.toast.show(error)
}
})

app 的 logo and name

android

LOGO

  • 将 logo 分别放到 android/app/src/main/res 下的四个文件夹中,命名为 ic_launcher.png
  • 没有对应上的文件夹放一张1024*1024或者512*512的即可

app 名称

  • android/app/src/main/res/values/strings.xml中更改

IOS

LOGO

  • xcode> 根目录 >Images.xcassets 将生成的图标拖入对应的位置

app 名称

  • xcode> 根目录 >Info.plist 中修改 Bundlename 和 General> Display Name

1、webpack 是什么

根据文档的定义:本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。

2、webpack 四个核心概念

  • 入口(entry) 入口,webpack 执行构建的第一步将从 Entry 开始,可抽象成输入
  • 输出(output) 输出结果,在 Webpack 经过一系列处理并得出最终想要的代码后输出结果
  • 加载器(loader) 模块转换器,用于把模块原内容按照需求转换成新内容。
  • 插件(plugins) 扩展插件,在 Webpack 构建流程中的特定时机会广播出对应的事件,插件可以监听这些事件的发生,在特定时机做对应的事情。

2.1entry

  • 定义的三种方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
第一种用法
module.exports = {
entry:'index.js'
}
第二种用法
module.exports = {
entry:['index.js','b.js']
}
第三种用法
module.exports = {
entry:{
index:'index.js',
b:'b.js'
}
}

推荐使用使用第三种方法,方便分清各个入口的定义

2.2output

  • 打包成的文件
  • 一个或多个
1
2
3
4
5
6
7
8
9
10
module.exports = {
entry:{
index:'index.js',
b:'b.js'
},
output:{
path:path.resolve(__dirname,'dist'),
filename:'[name].min.[hash:5].js'
}
}

在上面的例子中,

  • output 中的 name 和 entry 中的 key 相对应
  • output 的 filename 可以指定 hash。有两个值可以选择:
    • [hash]:hash 值是特定于整个构建过程的。
    • [chunkhash]:hash 值是特定于每一个文件的内容的。
      我们理想的缓存设计是,在一次版本更新(重新构建)后,只有当一个文件的内容确实发生了变化,它才需要被重新下载,否则应使用缓存。
      因此,以上两个值中更推荐的是[chunkhash]。你也可以阅读这篇官方的缓存指南了解更多细节。

2.3loader

  • 作用: 通过使用不同的 Loader,Webpack 可以要把不同的文件都转成 JS 文件,比如 CSS、ES6/7、JSX 等
  • 参数
    • test:匹配处理文件的扩展名的正则表达式
    • use:loader 名称,就是你要使用模块的名称
    • include/exclude:手动指定必须处理的文件夹或屏蔽不需要处理的文件夹
    • query:为 loaders 提供额外的设置选项
      下面以 css-loader 为例
1
2
3
4
5
6
7
8
9
10
11
    module: {
+ rules:[
+ {
+ test:/\.css$/,
+ use:['style-loader','css-loader'],
+ include:path.join(__dirname,'./src'),
+ exclude:/node_modules/
+ }
+ ]
},

  • 常用的 loader
    • 编译相关 babel-loader ts-loader
    • 样式相关 style-loader css-loader less-loader postcss-loader
    • 文件相关 file-loader url-loader

2.4plugins

  • 参与打包的整个过程
  • 打包优化和压缩
  • 配置编译时的变量
  • 用法(以压缩 js 插件为例)
1
2
3
4
5
module.exports = {
plugins: [
new UglifyjsWebpackPlugin()
]
}
  • 常用的 plugin
    • 优化相关
      • CommonsChunkPlugin
      • UglifyjsWebpackPlugin
    • 功能相关
      • ExtractTextWebpackPlugin
      • HtmlWebpackPlugin
      • HotModuleReplacementPlugin
      • CopyWebpackPlugin

3、webpack 的常用配置

3.1 配置开发服务器

1
npm i webpack-dev-server -D
1
2
3
4
5
6
7
+ devServer:{
+ contentBase:path.resolve(__dirname,'dist'),
+ host:'localhost',
+ compress:true,
+ port:8080
+ }

  • contentBase 配置开发服务运行时的文件根目录
  • host:开发服务器监听的主机地址
  • compress 开发服务器是否启动 gzip 等压缩
  • port:开发服务器监听的端口

3.2 自动产出 html

1
npm i html-webpack-plugin -D
1
2
3
4
5
6
7
8
9
   plugins: [
+ new HtmlWebpackPlugin({
+ minify: {
+ removeAttributeQuotes:true
+ },
+ hash: true,
+ template: './src/index.html',
+ filename:'index.html'
})]
  • minify 是对 html 文件进行压缩,removeAttrubuteQuotes 是去掉属性的双引号
  • hash 引入产出资源的时候加上哈希避免缓存
  • template 模版路径

3.3 分离 css

因为 CSS 的下载和 JS 可以并行,当一个 html 文件很大的时候,我们可以把 css 单独提取出来

1
npm install --save-dev extract-text-webpack-plugin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
   module: {
+ rules:[
+ {
+ test:/\.css$/,
+ use: ExtractTextWebpackPlugin.extract({
+ use:'css-loader'
+ }),
include:path.join(__dirname,'./src'),
exclude:/node_modules/
+ }
+ ]
},
plugins: [
+ new ExtractTextWebpackPlugin('css/index.css')]

3.4 编译 less 和 sass

1
2
npm i less less-loader -D
npm i node-saas sass-loader -D
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const cssExtract=new ExtractTextWebpackPlugin('css.css');
const lessExtract=new ExtractTextWebpackPlugin('less.css');
const sassExtract=new ExtractTextWebpackPlugin('sass.css');

{
test:/\.less$/,
use: lessExtract.extract({
use:['css-loader','less-loader']
}),
include:path.join(__dirname,'./src'),
exclude:/node_modules/
},
{
test:/\.scss$/,
use: sassExtract.extract({
use:['css-loader','sass-loader']
}),
include:path.join(__dirname,'./src'),
exclude:/node_modules/
},

3.5 处理 CSS3 属性前缀

为了浏览器的兼容性,有时候我们必须加入-webkit,-ms,-o,-moz 这些前缀

  • Trident 内核:主要代表为 IE 浏览器, 前缀为-ms
  • Gecko 内核:主要代表为 Firefox, 前缀为-moz
  • Presto 内核:主要代表为 Opera, 前缀为-o
  • Webkit 内核:产要代表为 Chrome 和 Safari, 前缀为-webkit
1
npm i postcss-loader autoprefixer -D
1
2
3
4
5
6
7
8
9
10
11
12
module.exports={
plugins:[require('autoprefixer')]
}
{
test:/\.css$/,
use: cssExtract.extract({
+ use:['css-loader','postcss-loader']
}),
include:path.join(__dirname,'./src'),
exclude:/node_modules/
},

postcss-loader其它用法可以参考文档

3.6 转义 ES6/ES7

Babel 其实是一个编译 JavaScript 的平台,可以把 ES6/ES7,React 的 JSX 转义为 ES5

1
npm i babel-core babel-loader babel-preset-env babel-preset-stage-0 babel-preset-react -D
1
2
3
4
5
6
7
8
9
10
11
{
test:/\.jsx?$/,
use: {
loader: 'babel-loader',
options: {
presets: ["env","stage-0","react"]
}
},
include:path.join(__dirname,'./src'),
exclude:/node_modules/
},

3.7 调试打包后的代码

webapck 通过配置可以自动给我们 sourcemaps 文件,map 文件是一种对应编译文件和源文件的方法

  • source-map 把映射文件生成到单独的文件,最完整最慢
  • cheap-module-source-map 在一个单独的文件中产生一个不带列映射的 Map
  • eval-source-map 使用 eval 打包源文件模块,在同一个文件中生成完整 sourcemap
  • cheap-module-eval-source-map sourcemap 和打包后的 JS 同行显示,没有映射列
1
2
3
module.exports = {
devtool:'eval-source-map'
}

3.8watch

当代码发生修改后可以自动重新编译

1
2
3
4
5
6
7
8
new webpack.BannerPlugin(''),

watch: true,
watchOptions: {
ignored: /node_modules/, //忽略不用监听变更的目录
aggregateTimeout: 500, //防止重复保存频繁重新编译,500毫秒内重复保存不打包
poll:1000 //每秒询问的文件变更的次数
},

3.9 拷贝静态文件

有时项目中没有引用的文件也需要打包到目标目录

1
npm i copy-webpack-plugin -D
1
2
3
4
new CopyWebpackPlugin([{
from: path.join(__dirname,'public'),//静态资源目录源地址
to:'./public' //目标地址,相对于output的path目录
}]),

3.10 打包先清空

1
npm i  clean-webpack-plugin -D
1
new cleanWebpackPlugin(path.join(__dirname,'dist'))

3.11 压缩 js /css

  • 压缩 JS 可以让输出的 JS 文件体积更小、加载更快、流量更省,还有混淆代码的加密功能
    npm i uglifyjs-webpack-plugin -D
    plugins: [ new UglifyjsWebpackPlugin()]

  • webpack 可以消除未使用的 CSS,比如 bootstrap 中那些未使用的样式

1
2
npm i -D purifycss-webpack purify-css
npm i bootstrap -S
1
2
3
4
5
6
7
8
9
{
test:/\.css$/,
use: cssExtract.extract({
use: [{
loader: 'css-loader',
options:{minimize:true}
},'postcss-loader']
}),
}
1
2
3
4
+ new PurifyCSSPlugin({
+ //purifycss根据这个路径配置遍历你的HTML文件,查找你使用的CSS
+ paths:glob.sync(path.join(__dirname,'src/*.html'))
+ }),

4、总结

1
以上是webpack核心概念总结,对概念的理解,有助于总体了解下webpack不同的作用,遇到相关问题,找对应的模块。

1.nodejs 的特点

1)单线程

在 Java、PHP 等服务器语言中,会为每一个客户端创建一个新的线程,而每个线程需要消耗大约 2MB 的内存。也就是说,一个 8GB 的内存可以满足 4000 人

的访问连接,这样就增加了服务器的硬件成本。Nodejs 不需要为每个用户的连接创建一个新的线程,而仅仅使用一个线程,一个 8GB 的内存可以满足
40000 人的连接。

  • 好处:操作系统不会再有创建线程、销毁线程的开销。
  • 坏处:如果一个用户导致了线程的奔溃,那么整个服务就奔溃了。

2)非阻塞 I/O

由于 Nodejs 采用了非阻塞 I/O 机制,因此在执行访问数据库的代码后,立即转而执行其后的代码,把数据库返回结果的代码处理放在了回调函数中,从而提高了
程序的执行效率。

当某个 I/O 执行完毕时,将以事件的形式通知执行 I/O 的线程,线程执行这个事件的回调函数,为了处理这个异步 I/O,线程必须有事件循环,不断的检查有没有未处理的事件,依次予以处理。

3)事件驱动

在 nodejs 中,客户端请求建立连接,数据提交等行为,会触发相应的事件。在 nodejs 中,在一个时刻只能执行一个事件的回调函数,但是在执行一个事件回调函数
的中途,可以转而处理其他事件,然后返回继续执行原事件的回调函数,这种处理机制,称为‘事件环’机制。

2.nodejs 适合开发什么样的业务?

1
2
3
4
当应用程序需要处理大量并发的I/O,而在向客户端响应之前,应用程序内部不需要进行非常复杂处理的时候,nodejs非常合适。nodejs也非常适合与websocket
配合,开发长连接的实时交互应用程序。
总之,nodejs擅长任务的调度,善于I/O,不善于计算。
比如:1)用户表单收集 2)考试系统 3)聊天室 4)图文直播 5)提供JSON的api

3.第一个简单的 node 程序

1
2
3
4
5
6
7
8
9
10
let http = require('http');
// 创建服务器,参数是一个回调函数,表示如果有请求进来要做什么
let server = http.createServer((req,res)=>{
// 设置http头部,状态码是200,文件类型是html,字符集是utf8
res.writeHead(200,{"Content-type":"text/html;charset=UTF-8"});
res.end('这是我的第一个Node页面');
})

// 运行服务器
server.listen(3000,'192.168.124.15')

问:如何将 html 页面展示到浏览器中呢

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
利用引入fs模块
let http = require('http');
let fs = require('fs');
let path = require('path');
let PULIC_PATH = path.resolve(__dirname,'01_helloWorld.html');

// 创建服务器,参数是一个回调函数,表示如果有请求进来要做什么
let server = http.createServer((req,res)=>{
fs.readFile(PULIC_PATH,(err,data)=>{
console.log('data',data,err);
// 设置http头部,状态码是200,文件类型是html,字符集是utf8
res.writeHead(200,{"Content-type":"text/html;charset=UTF-8"});
res.end(data);
});
})

// 运行服务器
server.listen(3000,'192.168.1.9')

问:如何根据不同的路由展示不同的页面?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
let http = require('http');
let fs = require('fs');
let PUBILC_PATH = require('path');

// 创建服务器,参数是一个回调函数,表示如果有请求进来要做什么
let server = http.createServer((req,res)=>{
if (req.url == '/sqaure') {
let path = PUBILC_PATH.resolve(__dirname,'01_helloWorld.html');
fs.readFile(path,(err,data)=>{
console.log('data',data,err);
// 设置http头部,状态码是200,文件类型是html,字符集是utf8
res.writeHead(200,{"Content-type":"text/html;charset=UTF-8"});
res.end(data);
});
} else if (req.url == '/circle') {
let path = PUBILC_PATH.resolve(__dirname,'01_helloWorld_circle.html');
fs.readFile(path,(err,data)=>{
console.log('data',data,err);
// 设置http头部,状态码是200,文件类型是html,字符集是utf8
res.writeHead(200,{"Content-type":"text/html;charset=UTF-8"});
res.end(data);
});
} else {
res.writeHead(404,{"Content-type":"text/html;charset=UTF-8"});
res.end('没有这个页面哦');
}

})

// 运行服务器
server.listen(3000,'192.168.1.9')

说明:在浏览器中输入http://192.168.1.9:3000/circle即可

4.http 模块与 url 模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
实例:
let http = require('http');
let url = require('url');

let server = http.createServer((req,res)=>{
let path = url.parse(req.url);
/*
Url {
protocol: null,
slashes: null,
auth: null,
host: null,
port: null,
hostname: null,
hash: null,
search: '?name=wq&age=20',
query: 'name=wq&age=20',
pathname: '/user',
path: '/user?name=wq&age=20',
href: '/user?name=wq&age=20'
}
*/
console.log('服务器接收到了请求1:',path);

// 将第二个参数设为true,就把query参数变为Object
let path2 = url.parse(req.url,true);
/*
Url {
protocol: null,
slashes: null,
auth: null,
host: null,
port: null,
hostname: null,
hash: null,
search: '?name=wq&age=20',
query: [Object: null prototype] { name: 'wq', age: '20' },
pathname: '/user',
path: '/user?name=wq&age=20',
href: '/user?name=wq&age=20'
}
*/
console.log('服务器接收到了请求2:',path2);

// 设置一个相应头
res.writeHead(200,{"Content-Type":"text/html;charset=UTF-8"});
// 每个请求都应该加上.end()方法,不然,浏览器请求会一直转菊花等待后端结束
res.end('<h1>响应头</h1>')
})

server.listen(3000,'192.168.1.9');

实操:做一个简单的表单提交

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<form action="http://192.168.1.9:3000" method="GET">
<input name='name' />
<input name='age' />
<input name='sex' />
<input type="submit">
</form>

let http = require('http');
let url = require('url');
let queryString = require('querystring');

let server = http.createServer((req,res)=>{
let queryObj = url.parse(req.url,true).query;
// querystring的作用与上句作用一样
let queryObj2 = queryString.parse(req.url.split('?')[1]);
console.log('参数',queryObj,queryObj2);

res.end('服务器收到了参数');
})

server.listen(3000,'192.168.1.9');

5.fs 模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
let http = require('http');
let fs = require('fs');

let server = http.createServer((req,res)=>{

let userID = parseInt(Math.random()*89999)+10000;
console.log(userID+'进入连接...')

res.writeHead(200, {'Content-Type': 'text/html;charset=utf-8','Access-Control-Allow-Origin':'*'});

// 读取文件
fs.readFile('../笔记.txt',(error,data)=>{
console.log(error,data);
if (error) {
throw error;
}
console.log(userID+'读取完毕...')
res.end();
})

// 创建文件夹
fs.mkdir(`./image/${userID}`);

// 读取文件状态
fs.stat('../笔记.txt',(error,stats)=>{
/*
Stats {
dev: 580307240,
mode: 33206,
nlink: 1,
uid: 0,
gid: 0,
rdev: 0,
blksize: 4096,
ino: 10696049115206348,
size: 7275, // 文件的大小(以字节为单位)
blocks: 16,
atimeMs: 1579832397095.3948, // 表明上次访问此文件的时间戳
mtimeMs: 1579872595571.8716, // 表明上次修改此文件的时间戳
ctimeMs: 1579872595571.8716, // 表明上次更改文件状态的时间戳
birthtimeMs: 1579832397095.3948, // 表明此文件的创建时间的时间戳
atime: 2020-01-24T02:19:57.095Z,
mtime: 2020-01-24T13:29:55.572Z,
ctime: 2020-01-24T13:29:55.572Z,
birthtime: 2020-01-24T02:19:57.095Z
}
*/
console.log('读取文件状态',stats);
console.log('是否是文件夹:',stats.isDirectory());
console.log('是否是文件:',stats.isFile());
res.end();
})

// 存储所有文件夹名
let dictionary = [];
// 查看文件夹中有多少文件
fs.readdir('../node',(error,files)=>{
// 以数组的形式输出node文件夹中所有的文件名
/*
[
'01_helloWorld.html',
'01_helloWorld.js',
'01_helloWorld_circle.html',
'02_helloWorld.js',
'02_表单提交.html',
'03_router.js',
'04_EventLoop.js'
]
*/
console.log('node文件夹下的文件名:',files);
files.map(item => {
fs.stat(`./${item}`,(error,stats)=>{
if (stats.isDirectory()) {
dictionary.push(item);
}
console.log('文件夹:'+dictionary);
})
})
res.end();
})
})

server.listen(3000,'192.168.1.9');

// 实例:获取某个文件夹中所有文件(夹)的名字
let http = require('http');

let fs = require('fs');

let server = http.createServer((req,res)=>{
fs.readdir('../node',(error,files)=>{
// 存放文件夹的数组
let dictionary = [];
(function iterator(i){
if (i == files.length) {
res.end('获取目录结束...');
return;
}
fs.stat(`../node/${files[i]}`,(err,stats)=>{
if (stats.isDirectory()) {
dictionary.push(files[i]);
console.log('dictionary',dictionary)
}
iterator(i+1);
})
})(0)
})
})

server.listen(3000,'192.168.1.9');

6.制作一个静态资源文件管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 意思是:新建一个static文件夹,文件夹下有1.html文件,可以通过http://192.168.1.9:3000/1.html去访问
let http = require('http');
let url = require('url');
let fs = require('fs');
let path = require('path');

let server = http.createServer((req,res)=>{
// 获取路径
let pathName = url.parse(req.url).pathname;
if (pathName == '/') {
pathName = 'index.html';
}
// 获取文件拓展名
let extname = path.extname(pathName);
console.log('获取文件拓展名',extname);

// 获取文件
fs.readFile('./static/'+pathName,(error,data)=>{
if (error) {
fs.readFile('./static/404.html',(err,errData)=>{
console.log('errrr',err,errData)
res.writeHead(404,{'Content-Type':'text/html;charset=UTF8'});
res.end(errData);
})
return;
}
res.writeHead(200,{"Content-Type":getMIME(extname)})
res.end(data);
})
})

server.listen(3000,'192.168.1.9');

function getMIME(extname){
switch(extname){
case ".html":
return "text/html";
break;
case ".jpg":
return "image/jpg";
break;
case ".css":
return "text/css";
break;
}
}

7.文件夹模块和 package 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
1)在js文件中引用,如果不写./ , 会默认查找node_modules文件夹下的文件

test.js文件
let foo = require('foo.js');
console.log(foo);

node_modules文件夹下foo.js文件
let a = 200;
exports.b = b;

2)在js文件中,如果引用不加扩展名,默认引用文件夹下index.js文件

test.js文件
let a = 'bar';
exports.a = a;

node_modules文件夹下bar文件夹下的index.js文件
let a = 'bar';
exports.a = a;

问:如果修改bar文件夹下index.js文件夹名为app.js,如何修改才能正常引用?
在bar文件夹下新建文件package.json,
package.json文件
{
"name": "app",
"version": "0.0.1",
"main": "app.js"
}

3)package.json管理依赖
在根文件夹下执行命令npm init,会生成package.json文件
package.json中:
"dependencies": {
"moment": "^2.24.0" // ^在谁前面就表示谁不变,在这里就表示2的大版本保持不变
},

4)路径注意问题
前提:同级目录下有a.js文件和text文件夹,text文件夹下有b.js和c.js
引用:
1.a引用b
let b = require('./text/b.js);
2.b引用c
let c = require('./c.js);
注意:引用都是从当前文件寻找其他文件
问:如果b.js中需要引入像fs等第三方模块读取文件时,如何引用?
使用绝对路径__dirname:
fs.readFile(__dirname+'/1.txt',()=>{})

8.POST 请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
<input id='name' />
<input id='age' />
<input id='submit' type="submit" value="提交" />
<script>
document.getElementById('submit').onclick=()=>{
let name = document.getElementById('name').value;
let age = document.getElementById('age').value;

let xhr = new XMLHttpRequest();
xhr.open('POST','http://192.168.1.9:3000');
xhr.addEventListener('load',function(){
console.log(this.response);
})
let obj = {
name,
age
};
xhr.send(JSON.stringify(obj));
}
</script>


let http = require('http');
let server = http.createServer((req,res)=>{
if (req.method == "POST") {
let allData = '';
req.on('data',(chunk)=>{
allData += chunk;
})
req.on('end',()=>{
res.writeHead(200,{'Content-Type':'text/html','Access-Control-Allow-Origin':'*'});
res.end(allData);
})
}
})
server.listen(3000,'192.168.1.9');

例:实现上传图片
前提:npm install formidable

<form action="http://192.168.1.9:3000" method="POST" enctype="multipart/form-data">
<input type="text" name='descript' />
<input type="file" name="file" />
<input type="submit" value="提交" />
</form>

let http = require('http');
let formidable = require('formidable');
let fs = require('fs');
let path = require('path');
let server = http.createServer((req,res)=>{
if (req.method == "POST") {
let form = new formidable.IncomingForm();
// 设置文件上传存储地址
form.uploadDir = './upLoads';
// 所有的文本域、单选框等都存放在fields中;所有的文件域都存放在files中
form.parse(req,(err,fields,files)=>{
// 修改文件名
let oldPath = __dirname + "/" + files.file.path;
let newPath = __dirname + "/upLoads" + "/" + fields.descript + path.extname(files.file.name);
console.log('修改文件名',oldPath,newPath);
fs.rename(oldPath,newPath,(error)=>{
if (error) {
throw Error('改名失败!');
}
res.writeHead(200,{"Content-Type":"text/plain;charset=utf-8"});
res.end('上传成功');
})
})
}
})
server.listen(3000,'192.168.1.9');

9.模板引擎

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
1)ejs模板引擎:是一种后端模板引擎

let ejs = require('ejs');

let str = '今天买了<%= a %>s';
let data = {
a:6,
}

let html = ejs.render(str,data)
console.log(html); // 今天买了6s

问:如何读取ejs页面?

index.ejs

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<h1>今天买了<%= a %>s</h1>
</body>
</html>

let ejs = require('ejs');
let fs = require('fs');
let http = require('http');

let server = http.createServer((req,res)=>{
fs.readFile("./views/ejs01.ejs",(err,data)=>{
let template = data.toString();
let obj = { a: 6};

let html = ejs.render(template,obj);

res.writeHead(200,{'Content-Type':'text/html;charset=utf-8','Access-Control-Allow-Origin':'*'});
res.end(html);
})
})
server.listen(8000,'127.0.0.1');

2)Jade模板引擎:也是一种后端模板引擎,省略了html标签

10.Express 框架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
Express是后端Node的框架。
前提:npm install express
1)初识Express
let express = require('express');

let app = express();


// express路由能力
app.get("/index",(req,res)=>{
res.send('首页');
})

app.get("/my",(req,res)=>{
res.send('我的');
})

app.get(/\/student\/([\d]{5})/,(req,res)=>{
res.send("学生信息,学号:"+req.params[0]);
})

app.get("/teacher/:teacherID",(req,res)=>{
res.send("老师信息,工号:"+req.params.teacherID);
})

// express引用静态文件能力(即以前放入Apache中的包)
app.use(express.static('./static'));

// express引用模板引擎能力
app.set('view engine','ejs');
app.get('/view',(req,res)=>{
res.render('ejs01',{
a: 11,
})
})

app.listen(3000);

2)如果想要处理任何类型的请求(get,post)就使用app.all
app.all("/",(req,res)=>{

})

3)Restful路由设计:简单来说,就是同一个路由路径,根据不同的请求类型,展现出不同的功能

4)中间件
如果GET、POST请求的回调函数中,没有next参数,那么就会匹配第一个路由,不会继续往下匹配;如果想要继续往下匹配,就需要next()

let express = require('express');

let app = express();

app.get('/',(req,res,next)=>{
console.log('1');
next();
})

app.get('/',(req,res)=>{
console.log('2');
})

app.listen(3000);

1)app.use()是一个中间件,这与get/post不同的是,app.use()的网址不是精确匹配的,是可以扩展的
let express = require('express');

let app = express();

app.use("/admin",(req,res)=>{
// 当用户输入 http://127.0.0.1:3000/admin/userinfo/001
console.log(req.originalUrl); // 用户输入访问的网址 /admin/userinfo/001
console.log(req.baseUrl); // use中的第一个参数 /admin
console.log(req.path); // 用户网址减去use中的第一个参数 /userinfo/001
res.end();
})

app.listen(3000);

2)当app.use不写路径时,实际上是相当于"/",代表所有网址
app.use((req,res,next)=>{
next();
})

3)静态服务
let express = require('express');

let app = express();

// 当用户访问http://127.0.0.1:3000/static/即可访问static文件夹下文件
app.use('/static',express.static('./static'));

// use函数会自动识别err这个参数,如果有就能自动捕获
app.use((req, res)=>{
res.send('不存在这个页面!');
})

app.listen(3000);

4)render()和send()
①大多数情况下,渲染内容用res.render(),将会根据views中的模板文件进行渲染。如果渲染不想使用views文件夹,可以:
app.set('views',__dirname+'/Views2');
②如果想写一个快速测试页,当然可是使用res.render()。这个函数将根据内容,自动帮我们设置了Content-Type头部和200状态码
③如果想要使用不同的状态码,可以:
res.status(400).render('we connot find it')
④如果想要设置不同的Content-Type,可以:
res.set('Content-Type','text-html');

5)express的GET和POST请求
①GET请求
let express = require('express');

let app = express();

app.get('/',(req,res)=>{
// 获取参数
// 当访问http://127.0.0.1:3000/?name=wq&id=1270837469
console.log(req.query); // { name: 'wq', id: '1270837469' }
res.send();
})

app.listen(3000);
②POST请求

前提:npm install body-parser
let bodyParser = require('body-parser');
app.use(bodyParser.urlencoded({extended:false}));
app.post('/',(req,res)=>{
res.send(JSON.stringify(req.body));
})

11.NoSQL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
定义:非结构性数据库,没有行和列的概念,用JSON来存储数据,代替了老牌数据库表和行的概念,分别使用集合和文档。

mongodb的一些命令:
mongo:使用数据库
mongod:开机
mongoimport:导入数据

启动mongodb:
新建一个文件夹我的目录是:F:\mongo
打开cmd,执行 mongod --dbpath F:\mongo
新打开一个cmd,执行 mongo

mongodb命令:
show dbs:展示当前所有数据库
use 数据库名:使用指定数据库;或者新建数据库
db:查看当前所在数据库
插入数据:
db.数据库名.insert({"name":"wq"})
查看当前数据库集合:
show collections
查看当前集合中的文档:
db.数据库名.find()
查询name字段为'wq'的文档:
db.数据库名.find({"name":"wq"})
删除数据库:(当前所在的数据库)
db.dropDatabase()
导入数据库:
mongoimport --db 数据库名 --collection 集合名 --drop --file 文件路径
注意: --drop 代表删除之前集合中存在的数据
查询:
数据结构:
{
"name":"wq",
"age":23,
"hobby":["sleep","eat"],
"score":{
"yuwen":89,
"shuxue":100
},
"points": [
{ "points": 78, "bonus": 8 },
{ "points": 57, "bonus": 7 }
]
}
1)查询数学成绩为100的学生
db.student.find({"score.shuxue":100})
2)查询年龄为23并且数学成绩为100的学生
db.student.find({"score.shuxue":100,"age":23})
3)查询语文成绩大于70分的学生($gt)
db.student.find({"score.yuwen":{$gt:70}})
4)查询语文成绩小于70分的学生($lt)
db.student.find({"score.yuwen":{$lt:70}})
4)查询语文成绩小于70或者年龄小于20岁的学生
db.student.find({$or:[{"score.yuwen":{$lt:70}},{"age":{$lt:20}}]})
5)查询年龄大于15岁的学生并且按照语文成绩升序排列(1代表升序,-1代表降序),如果有两个参数,则先按前者排序,后按后者排序
db.student.find({"age":{$gt:15}}).sort({"age":1})
6)查询爱好为睡觉的学生
db.student.find({"hobby":"sleep"})
7)查询第二个爱好为eat的学生
db.student.find({"hobby.1":"eat"})
8)查询bonus大于7的学生
db.student.find({"points":{$elemMatch:{"bonus":{$gt:7}}}})
9)查询hobby为eat和sleep的学生
db.student.find({"hobby":{$all:["eat","sleep"]}})
10)查询爱好为2个的学生
db.student.find({"hobby":{$size:2}})

删除
1)删除集合
db.集合名.drop()
2)删除集合中名字为wq的学生
db.student.remove({"name":"wq"})
注意:只删除一个添加{justOne:true}
db.student.remove({"name":"wq"},{justOne:true})
修改
1)修改名字为wq2的学生年龄为21
db.student.update({"name":"wq2"},{$set:{"age":21}})
2)修改名字满足wq\d{1,}所有学生的年龄为33
db.student.updateMany({"name":/wq\d{1,}/},{$set:{"age":33}})
3)完全替换名字为wq2
db.student.update({"name":"wq2"},{"name":"wq2","age":1})
4)添加points
db.students.update({"name":"wq"},{$addToSet:{"points":{ "points": 58, "bonus": 7 }}})

12.Nodejs 连接 Mongodb 数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
let express = require('express');

let app = express();

let MongoClient = require('mongodb').MongoClient;

app.get('/',(req,res)=>{
// itcase代表数据库
let url = "mongodb://127.0.0.1:27017/itcase";
MongoClient.connect(url,(err,client)=>{
if (err) {
console.log('数据库连接失败!')
return;
}
console.log('数据库连接成功!')
let db = client.db("itcase");
db.collection('student').insertOne({
"name":"wcc",
"age":30
},(error,result)=>{
if (error) {
console.log('数据插入失败!')
return;
}
console.log('result',result);
res.send();
})
})
})

app.listen(3000);

13.nodejs 操作 mongodb(增删改查)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
let MongoClient = require('mongodb').MongoClient;

// 连接数据库
function connectMongodb(dbName,callback){
let url = `mongodb://127.0.0.1:27017/${dbName}`;
MongoClient.connect(url,callback)
}

// 插入数据
exports.insertOne = (dbName,collectionName,data,callback) => {
connectMongodb(dbName,(err,client)=>{
if (err) {
console.log('数据库连接失败!')
return;
}
let db = client.db(dbName);
db.collection(collectionName).insertOne(data,callback);
})
}

// 查询数据
exports.find = (dbName,collectionName,condition,pageSize,pageNo,callback) =>{
connectMongodb(dbName,(err,client)=>{
let db = client.db(dbName);
let cursor = db.collection(collectionName).find(condition).limit(pageSize).skip((pageNo-1)*pageSize);
let result = [];
cursor.each((error,doc)=>{
if (doc!=null) {
result.push(doc);
}else {
callback(null,result);
}
})
})
}

// 删除数据
exports.delete = (dbName,collectionName,condition,callback)=>{
connectMongodb(dbName,(err,client)=>{
let db = client.db(dbName);
db.collection(collectionName).deleteMany(condition,(error,result)=>{
if(error){
callback('删除失败!',null);
return;
}
callback(null,result);
})
})
}

// 修改数据
exports.update = (dbName,collectionName,searchCondition,updateData,callback)=>{
connectMongodb(dbName,(err,client)=>{
let db = client.db(dbName);
db.collection(collectionName).updateOne(searchCondition,{$set:updateData},(error,result)=>{
if(error){
callback('修改失败!',null);
return;
}
callback(null,result);
})
})
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
Cookie:
1)Http是无状态协议,简单的说,当你浏览一个页面,然后转到同一个网站的另一个页面,服务器无法认识到这是同一个浏览器在访问同一个网站。每一次访问都是没有关系的。
2)Cookie是一个简单到爆的想法:当访问一个页面的时候,服务器在下行Http报文中,命令浏览器存储一个字符串;当浏览器再次访问同一个域的时候,将把这个字符串携带到
上行Http请求中。
特点:
1)Cookie不加密,用户可以自由看到
2)用户可以删除Cookie或者禁用它
3)Cookie可以被篡改
4)Cookie可以用于攻击
5)Cookie的存储量小

node中的Cookie:
前提:npm install cookie-parser
例子:
let express = require('express');
let cookieParser = require('cookie-parser');
let app = express();
app.use(cookieParser());

app.get('/',(req,res)=>{
console.log('请求Cookie',req.cookies)
// maxAge:Cookie的有效期
res.cookie('name','wq',{maxAge:24*60*60*1000});
res.send();
})
app.listen(3000);

Session:
Session不是一开始就有的技术,而是依赖Cookie。当浏览器禁用cookie或者用户清除cookie的时候登录效果就消失了。
Session下发的乱码是乱码,并且服务器自己缓存一些东西,下次服务器带着乱码请求,与缓存比对,就知道请求用户是谁了。

node中的Session:
前提:npm install express-session
let express = require('express');
let app = express();

let session = require('express-session');

app.use(session({
secret:'wq',
resave:false,
saveUninitialized:true
}))

app.get('/',(req,res)=>{
if (req.session.userInfo) {
res.send('欢迎您,'+req.session.userInfo);
return;
}
res.send('您未登录,请先登录!!')
})

app.get('/login',(req,res)=>{
req.session.userInfo = '王清'; // 设置Session
res.send('您已成功登录!');
})

app.listen(3000);

14.MD5 加密

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    MD5加密是函数型加密,就是每次加密的结果是一样的,没有随机位。
特点:
1)无论需要加密的文字多长多短,永远是32位。
2)哪怕只改一个字,密文都会大变。

node中的加密:
前提:npm install crypto
let express = require('express');
let app = express();
let crypto = require('crypto');

app.get('/',(req,res)=>{
// 选择加密方式,一般有:sha1,md5,sha256,sha512
let md5 = crypto.createHash('md5');
let password = md5.update('wq').digest('base64');
res.send("加密之后的密码:"+password);
})

app.listen(3000);

16.Mongoose

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
简介:是一个将JavaScript对象与数据库产生联系的框架。Object Related Model(ORM:对象关系模型)。操作对象就是操作数据库
前提:npm install mongoose
例子demo:
let mongoose = require('mongoose');
// 连接数据库
mongoose.connect('mongodb://127.0.0.1/animals');

// 创建模型:相当于创建了一个类,每个Cat实例都有name和age属性,在mongo中相当于文档
let Cat = mongoose.model('cat',{name:String,age:Number});

// 实例化
let Ketty = new Cat({name:'Ketty',age:12});

// 保存到数据库
Ketty.save((err,result)=>{
console.log(err,result);
})
封装增删改查:
前提:在同一目录下新建models文件夹和app.js文件,models文件夹下新建db.js和student.js;
代码:
app.js:
let db = require('./models/db');

let Student = require('./models/student');

// 添加方法一:
let xiaoming = new Student({name:'小明',age:18})
xiaoming.save(()=>{console.log('添加小明成功!')})

// 添加方法二:
Student.create({name:'小红',age:20,sex:'女'},()=>{console.log('添加小红成功!')});

// 查找方法一:利用自定义的静态方法查找
Student.findByName('小红',(err,result)=>{console.log(result)})

// 查找方法二:
Student.find({name:'小红'},(err,result)=>{console.log(result)});

// 修改小红年龄为30岁:这里可以不用添加$set为{$set:{age:30}}
Student.updateOne({name:'小红'},{age:30},(err,result)=>{console.log(result)})

// 修改方法二:
Student.find({name:'小红'},(err,result)=>{
let xiaohong = result[0];
xiaohong.age=30;
xiaohong.save();
});

// 删除小红
Student.deleteOne({name:'小红'},(err,result)=>{console.log(result)})

// 使用实例方法
let xiaoqiang = new Student({name:'小强',age:22});
xiaoqiang.console();
db.js:
let mongoose = require('mongoose');

// 创建连接,给每个用户都会创建一个连接
let db = mongoose.createConnection('mongodb://127.0.0.1:27017/school')

db.once('open',()=>{
console.log('数据库连接成功!');
})

module.exports = db;
student.js:
let mongoose = require('mongoose');
let db = require('./db');

let studentSchema = new mongoose.Schema({
name : {type:String},
age : {type:Number},
sex : {type:String,default:'男'}
})

// 创建查询静态方法
studentSchema.statics.findByName = (name,callback) => {
db.model('student').find({name},callback);
}

/**
* 创建更改静态方法
* conditions:修改条件
* data:改成data
* options:可选参数,它有如下属性
* safe :(布尔型)安全模式(默认为架构中设置的值(true))
upsert :(boolean)如果不匹配,是否创建文档(false)
multi :(boolean)是否应该更新多个文档(false)
runValidators:如果为true,则在此命令上运行更新验证程序。更新验证器根据模型的模式验证更新操作。
strict:(布尔)覆盖strict此更新的选项
overwrite: (布尔)禁用只更新模式,允许您覆盖文档(false)
*/
studentSchema.statics.update = (conditions,data,options,callback) => {
db.model('student').update(conditions,{$set:data},options,callback);
}

// 定义实例方法
studentSchema.methods.console = ()=>{
console.log('这是实例方法')
}

let studentModel = db.model('student',studentSchema);

module.exports = studentModel;

17.WebSocket 和 Socket.IO 框架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
WebSocket允许客户端和服务器以全双工的方式进行通信;WebSocket的原理:利用HTTP请求产生握手之后,二者转用TCP协议进行交流(QQ协议)

方法一:利用socket.io模块
前提:npm install socket.io
模拟websocket:
前端代码studyWebsocket.html:
<h1>模拟websocket</h1>
<input id='content' /><button id='send'>发送</button>
<script src="https://cdn.bootcss.com/socket.io/2.2.0/socket.io.dev.js"></script>
<script>
var socket = io.connect('http://127.0.0.1:3000');
document.getElementById('send').onclick = ()=>{
socket.emit('question',document.getElementById('content').value);
}
socket.on('answer',data=>console.log(data));
</script>
Node代码:
(
如果使用express框架:
const express = require('express');
const app = express();
let server = require('http').createServer(app);
let io = require('socket.io')(server);
)
let http = require('http');
let fs = require('fs');

let server = http.createServer((req,res)=>{
if (req.url == '/') {
fs.readFile('./studyWebsocket.html',(err,data)=>{
res.end(data);
})
}
})

let io = require('socket.io')(server);

// 监听连接事件
// 对于前端和后端的socket对象,都有emit和on方法,emit发送请求,on接受请求
io.on('connection',socket=>{
socket.on('question',data=>{
socket.emit('answer',data);
})
// 广播:所有连接到此websocket上来的用户,如果其中一个用户发起thank请求,所有用户都能得到回应
io.emit('broadcast',{'say':'谢谢'});
})

server.listen(3000,'127.0.0.1');

方法二:利用ws模块
前端代码:
<h1>模拟websocket</h1>
<input id='content' /><button id='send'>发送</button>
<script>
var ws = new WebSocket('ws://127.0.0.1:3000');
ws.addEventListener('open',()=>{
ws.send('连接成功...');
})
document.getElementById('send').onclick = ()=>{
// 给服务器发送一个字符串:
ws.send(document.getElementById('content').value);
}
ws.onmessage = result=>{
console.log(result.data);
}
</script>

node代码:
const WebSocket = require('ws');
// 实例化:
const wss = new WebSocket.Server({
port: 3000
});

// 监听连接事件
wss.on('connection',(ws)=>{
// 接受对方发送过来的信息
ws.on('message',data=>{
ws.send(data);
})
})

angularjs 简单介绍和特点

首先 angular 是一个 mvc 框架, 使用 mvc 解耦, 采用 model, controller 以及 view 的方式去组织代码, 会将一个 html 页面分成若干个模块, 每个模块都有自己的 scope, service, directive, 各个模块之间也可以进行通信, 但是整体结构上是比较清晰的, 就是说其代码组织是模块化的, angular 的 view 可能仅仅是一个框架, , 对 view 的 dom 操作或者事件监听都是在 directive 中实现的, 而且一般情况下很少直接去写 dom 操作代码, 只要你监听 model, model 发生变化后 view 也会发生变化, 就是双向绑定机制, angularjs 适用于单页面开发

在 angularJS 中,一个模板就是一个 HTML 文件。但是 HTML 的内容扩展了,包含了很多帮助你映射 model 到 view 的内容。

HTML 模板将会被浏览器解析到 DOM 中。DOM 然后成为 AngularJS 编译器的输入。AngularJS 将会遍历 DOM 模板来生成一些指导,即,directive(指令)。所有的指令都负责针对 view 来设置数据绑定。

angularJS 并不把模板当做 String 来操作。输入 angularJS 的是 DOM 而非 string。数据绑定是 DOM 变化,不是字符串的连接或者 innerHTML 变化。使用 DOM 作为输入,而不是字符串,是 AngularJS 区别于其它的框架的最大原因。使用 DOM 允许你扩展指令词汇并且可以创建你自己的指令,甚至开发可重用的组件。angular 占用的内存较小, 可以兼容主流的浏览器, 他拥有内置的依赖注入的子系统, 可以帮助开发人员更容易开发, 理解和测试和应用, DI 允许你请求你的依赖,而不是自己找寻它们。比如,我们需要一个东西,DI 负责找创建并且提供给我们。那么 AngularJS 可以做到。指令可以用来创建自定义的标签。它们可以用来装饰元素或者操作 DOM 属性。

vuejs 简单介绍特点

官网: http://cn.vuejs.org/

vue 是一个渐进式的框架, 是一个轻量级的框架, 也不算是一个框架, 他核心只关注图层, 是一个构建数据驱动的 web 界面,易于上手, 还便于第三方库或与既有项目整合,也能够为复杂的单页应用程序提供驱动

1. vue 的核心

是一个允许采用简洁的模板语法来声明式的将数据渲染进 dom

先创建一个后缀名为.html 的文件

代码如下:

1
2
3
4
5
6
7
8
html:  <div id="app"></div>

js里面的 var app = new Vue({
      el: '#app',
      data: {
       msg: 'Hello Vue!'
      }
   })

在浏览器窗口上出现的内容:  Hello Vue
数据和 DOM 已经绑定在一起, 验证是否是响应式的, 修改控制台里面 app.msg,你就会看到上面渲染是列子也更新出来了文本插值, 还可以用绑定 DOM 元素属性

1
2
3
4
5
6
7
8
9
10
11
12
<div id="app-2">
<span v-bind:title="message">
   查看此处动态绑定提示信息!
</span>
</div>
js代码:
var app2 = new Vue({
  el: '#app-2',
  data: {
     message: '页面加载于 ' + new Date()
   }
})

2. vue 指令

指令带有前缀 v-, 以表示他们是 Vue 提供的特殊属性, 他们会在渲染的 DOM 上应用特殊的响应式行为

v-bind : str = “msg” 将这个元素的节点的 str 属性和 vue 实例对象的 msg 属性保持一致

v-if = 布尔值 条件渲染指令, 根据其后表达式的布尔值进行判断是否渲染该元素, v-if 只会渲染他身后表达式为 true 的元素

v-show = 布尔值 和 v-if 类似,只是会渲染他身后表达式为 false 的元素, 而且会给这样的元素添加 css 的代码, style = “display:none”

v-else 必须在 v-if/v-show 指令后, 不然就不会起作用, 如果 v-if/v-show 的指令表达式为 true, 则 else 就不显示, 如果 v-if/v-show 指令的表达式为 false, 则 else 元素会显示在页面上

v-for 类似于 js 的遍历, 用法为 v-for=”(item,index) in imgs” :key=”index”, items 是数组, item 为数组中的数组元素, index 是索引号, key 是为了更高效的查找到指定元素

v-on 用于监听指定元素的 DOM 事件 v-on:click=”greet”

3. vue 的双向数据绑定

vueJS 是使用 ES5 提供的 Object.defineProperty()方法, 监控对数据的操作, 从而可以自动触发数据, 并且, 由于是在不同的数据上触发同步, 可以精确的将变更发送给绑定的视图, 而不是对所有的数据都执行一次检测

vue 和 angular 中, 都是通过在 html 中添加指令的方式, 将视图元素与数据的绑定关系进行声明

1
2
3
<form id= "app">
  <input type="test" v-model="name">
</form>

以上的 html 代码表示该 input 元素与 name 数据进行绑定, 在 js 代码总可以这样进行初始化

1
2
3
4
5
6
var vm = new Vue({
  el: "#app",
  data:{
   name: "请输入你的名字"
  }
})

代码执行正确后, 页面上 input 元素对应的位置会显示上面的代码给出的初始值 “请输入你的名字”, 由于双向绑定数据已经建立, 因此, 在 vm.name=”小米”, 则 input 也会更新为小明, 在页面 input 上输入小明, 则 vm.name 获取的值为小明

4. vue 的插件化

插件通常会为 vue 添加全局功能, 插件的范围没有限制

添加全局的方法或者属性 vue-element 这个我并不是很懂

添加全局资源 指令/过滤器/ 过渡

添加 vue 实例的办法, 将他们添加到 vue-prototype 上实现

引入一个库, 来提供自己的 api, 同时提供上面的一个或者多个功能, 如 vue-router

1
import vueRouter from ''vue-router';  //使用webpack的单文件组件打包的方式 会调用vue.component来注册全局组件或者vue.components注册局部组件

如果是后者,每个单文件组件中都不需要引入 vue,
因为单文件组件经 webpack 打包后,生成的模块只是一个组件选项对象,被其他组件或 Vue 实例注册时使用语法糖,只需要 字面量对象的 组件选项对象就可以了。

使用插件:

1
vue.use(vueRouter); //通过全局方法Vue.use()使用插件, 会阻止注册相同插件多次, 只会注册一次该插件

angular 和 jquery 的区别

angular 中是尽量避免操作 DOM, angular 是基于数据驱动, 适合做数据操作比较繁琐的项目,angular 适用于单页面开发,是一个比较完善的 mvvm 框架, 包含模板和双向数据绑定, 路由, 模块化, 服务, 过滤器, 依赖注入等所有功能,但是 angular 验证功能比较薄弱, 需要写很多模板标签, 而且 ngview 只能有一个, 不能嵌套多个视图,angular 的兼容性比较好, jquery 是基于操作 DOM, 适用于操作 DOM 比较多的项目, jquery 是一个库, 比较大,兼容大部分浏览器, 有丰富的插件, 可拓展性强, jquery 不能向后兼容, 使用插件时,可能会有冲突,

angular 和 vue 的差别

一 angular 是 mvvm 框架, 而 vue 是一个渐进式的框架, 相当于 view 层, 都有双向数据绑定, 但是 angular 中的双向数据绑定是基于脏检查机制, vue 的双向数据绑定是基于 ES5 的 getter 和 setter 来实现, 而 angular 是有自己实现一套模板编译规则,vue 比 angular 更轻量, 性能上更高效, 比 angular 更容易上手, 学习成本低, vue 需要一个 el 对象进行实例化, 而 angular 是整个 html 页面下的,单页面应用, 而 vue 可以有多个 vue 实例

一、Enum 类型

使用枚举我们可以定义一些带名字的常量。 使用枚举可以清晰地表达意图或创建一组有区别的用例。 TypeScript 支持数字的和基于字符串的枚举。

1.数字枚举

1
2
3
4
5
6
7
8
enum Direction {
NORTH,
SOUTH,
EAST,
WEST,
}

let dir: Direction = Direction.NORTH;

默认情况下,NORTH 的初始值为 0,其余的成员会从 1 开始自动增长。换句话说,Direction.SOUTH 的值为 1,Direction.EAST 的值为 2,Direction.WEST 的值为 3。上面的枚举示例代码经过编译后会生成以下代码:

1
2
3
4
5
6
7
8
9
"use strict";
var Direction;
(function (Direction) {
Direction[(Direction["NORTH"] = 0)] = "NORTH";
Direction[(Direction["SOUTH"] = 1)] = "SOUTH";
Direction[(Direction["EAST"] = 2)] = "EAST";
Direction[(Direction["WEST"] = 3)] = "WEST";
})(Direction || (Direction = {}));
var dir = Direction.NORTH;

当然我们也可以设置 NORTH 的初始值,比如:

1
2
3
4
5
6
enum Direction {
NORTH = 3,
SOUTH,
EAST,
WEST,
}

2.字符串枚举

在 TypeScript 2.4 版本,允许我们使用字符串枚举。在一个字符串枚举里,每个成员都必须用字符串字面量,或另外一个字符串枚举成员进行初始化。

1
2
3
4
5
6
enum Direction {
NORTH = "NORTH",
SOUTH = "SOUTH",
EAST = "EAST",
WEST = "WEST",
}

以上代码对于的 ES5 代码如下:

1
2
3
4
5
6
7
8
"use strict";
var Direction;
(function (Direction) {
Direction["NORTH"] = "NORTH";
Direction["SOUTH"] = "SOUTH";
Direction["EAST"] = "EAST";
Direction["WEST"] = "WEST";
})(Direction || (Direction = {}));

3.异构枚举

异构枚举的成员值是数字和字符串的混合:

1
2
3
4
5
6
7
8
enum Enum {
A,
B,
C = "C",
D = "D",
E = 8,
F,
}

以上代码对于的 ES5 代码如下:

1
2
3
4
5
6
7
8
9
10
"use strict";
var Enum;
(function (Enum) {
Enum[Enum["A"] = 0] = "A";
Enum[Enum["B"] = 1] = "B";
Enum["C"] = "C";
Enum["D"] = "D";
Enum[Enum["E"] = 8] = "E";
Enum[Enum["F"] = 9] = "F";
})(Enum || (Enum = {}));

通过观察上述生成的 ES5 代码,我们可以发现数字枚举相对字符串枚举多了 “反向映射”:

1
2
console.log(Enum.A) //输出:0
console.log(Enum[0]) // 输出:A

二、特殊数据类型

1. Unknown 类型

就像所有类型都可以赋值给 any,所有类型也都可以赋值给 unknown。这使得 unknown 成为 TypeScript 类型系统的另一种顶级类型(另一种是 any)。下面我们来看一下 unknown 类型的使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
let value: unknown;

value = true; // OK
value = 42; // OK
value = "Hello World"; // OK
value = []; // OK
value = {}; // OK
value = Math.random; // OK
value = null; // OK
value = undefined; // OK
value = new TypeError(); // OK
value = Symbol("type"); // OK

value 变量的所有赋值都被认为是类型正确的。但是,当我们尝试将类型为 unknown 的值赋值给其他类型的变量时会发生什么?

1
2
3
4
5
6
7
8
9
10
let value: unknown;

let value1: unknown = value; // OK
let value2: any = value; // OK
let value3: boolean = value; // Error
let value4: number = value; // Error
let value5: string = value; // Error
let value6: object = value; // Error
let value7: any[] = value; // Error
let value8: Function = value; // Error

unknown 类型只能被赋值给 any 类型和 unknown 类型本身。直观地说,这是有道理的:只有能够保存任意类型值的容器才能保存 unknown 类型的值。毕竟我们不知道变量 value 中存储了什么类型的值。

现在让我们看看当我们尝试对类型为 unknown 的值执行操作时会发生什么。以下是我们在之前 any 章节看过的相同操作:

1
2
3
4
5
6
7
let value: unknown;

value.foo.bar; // Error
value.trim(); // Error
value(); // Error
new value(); // Error
value[0][1]; // Error

value 变量类型设置为 unknown 后,这些操作都不再被认为是类型正确的。通过将 any 类型改变为 unknown 类型,我们已将允许所有更改的默认设置,更改为禁止任何更改。

2. Never 类型

never 类型表示的是那些永不存在的值的类型。 例如,never 类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型。

1
2
3
4
5
6
7
8
// 返回never的函数必须存在无法达到的终点
function error(message: string): never {
throw new Error(message);
}

function infiniteLoop(): never {
while (true) {}
}

在 TypeScript 中,可以利用 never 类型的特性来实现全面性检查,具体示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
type Foo = string | number;

function controlFlowAnalysisWithNever(foo: Foo) {
if (typeof foo === "string") {
// 这里 foo 被收窄为 string 类型
} else if (typeof foo === "number") {
// 这里 foo 被收窄为 number 类型
} else {
// foo 在这里是 never
const check: never = foo;
}
}

注意在 else 分支里面,我们把收窄为 never 的 foo 赋值给一个显示声明的 never 变量。如果一切逻辑正确,那么这里应该能够编译通过。但是假如后来有一天你的同事修改了 Foo 的类型:

1
type Foo = string | number | boolean;

然而他忘记同时修改 controlFlowAnalysisWithNever 方法中的控制流程,这时候 else 分支的 foo 类型会被收窄为 boolean 类型,导致无法赋值给 never 类型,这时就会产生一个编译错误。通过这个方式,我们可以确保

controlFlowAnalysisWithNever 方法总是穷尽了 Foo 的所有可能类型。 通过这个示例,我们可以得出一个结论:使用 never 避免出现新增了联合类型没有对应的实现,目的就是写出类型绝对安全的代码。

三、Typescript 的类型系统

充分使用编辑器的 language service 功能(类型提示,类型检查,类型推倒,自动补全,类型定义跳转)

把类型当做值的集合思考

1
2
3
4
5
6
7
8
9
type A= 'A' // 单值集合 { 'A' }
type B= 'B' // 单值集合 { 'B' }
type AB = 'A' | 'B' // 集合的并集 { 'A', 'B' }
type twoInt = 2 | 4 | 5 ... // 无限元素集合 { 1,2,3,4}
type threeInt = 3 | 6 | 9 // 无限集合
type twoIntersectThreeInt = twoInt & threeInt // 无限集合的交集
type twoUnionThreeInt = 2| 3 | 4 | 6 ... // 无限集合的并集
keyof (A&B) = (keyof A) | (keyof B)
keyof (A|B) = (keyof A) & (keyof B)

术语和集合术语对照表

1
2
3
4
5
6
7
8
9
Typescript术语	                  集合术语
never 空集
literal type 单值集合
value 可赋值给 T value ∈T
T1 assignable to T2 T1是T2的子集
T1 extends T2 T1是T2的子集
T1 T2
T1 & T2 T1 和T2的交集
unknown universal set

四、了解 type 和 interface 的区别

绝大部分情况下,type 和 interface 都能等价转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// 普通对象
type TState = {
name: string;
capital: string;
}
interface IState {
name: string;
capital: string;
}
// index signature
type TDict = {[key:string]: string }
interface IDict {
[key:string]: string
}
type TFn = (x:number) => string;
interface IFn {
(x:number):string;
}
// function with props
type TFnWithProps = {
(x:number):number;
prop: string;
}
interface IFnWithProps {
(x:number):number;
prop: string;
}
// constructor
type TConstructor = new(x:number) => {x:number}
interface IConstructor{
new(x:number): {x:number}
}
// generic
type TPair<T>= {
first: T;
second: T;
}
interface IPair<T> {
first: T;
second: T;
}
// extends
type TStateWithProps = TState & { population : number}
interface IStateWithProp extends IState {
population: number;
}
// implements
class StateT implements TState {
name = '';
capital = '';
}
class StateI implements IState {
name='';
capital = ''
}

type 和 interface 亦有所区别

  1. interface 无法应用于 union type | intersection type | conditional type | tuple
1
2
3
type AorB = 'A' | 'B'
type NamedVariable = (Input | Output) & { name: string}
type Pair = [number,number]
  1. interface 可以 argumented,而 type 不可以
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// inner
interface IState {
name :string;
capital: string;
}
// outer
interface IState {
population: number
}
const wyoming: IState = {
name: 'Wyoming',
capital: 'Cheyenne',
population: 500_000
}

五、keyof 操作符

keyof 简介

TypeScript 允许我们遍历某种类型的属性,并通过 keyof 操作符提取其属性的名称。keyof 操作符是在 TypeScript 2.1 版本引入的,该操作符可以用于获取某种类型的所有键,其返回类型是联合类型。

下面我们来看个例子:

1
2
3
4
5
6
7
8
9
interface Person {
name: string;
age: number;
location: string;
}

type K1 = keyof Person; // "name" | "age" | "location"
type K2 = keyof Person[]; // number | "length" | "push" | "concat" | ...
type K3 = keyof { [x: string]: Person }; // string | number

除了接口外,keyof 也可以用于操作类,比如:

1
2
3
4
5
6
class Person {
name: string = "Semlinker";
}

let sname: keyof Person;
sname = "name";

若把 sname = "name" 改为 sname = "age" 的话,TypeScript 编译器会提示以下错误信息:

1
Type '"age"' is not assignable to type '"name"'.

keyof 操作符除了支持接口和类之外,它也支持基本数据类型:

1
2
3
let K1: keyof boolean; // let K1: "valueOf"
let K2: keyof number; // let K2: "toString" | "toFixed" | "toExponential" | ...
let K3: keyof symbol; // let K3: "valueOf"

此外 keyof 也称为输入索引类型查询,与之相对应的是索引访问类型,也称为查找类型。在语法上,它们看起来像属性或元素访问,但最终会被转换为类型:

1
2
3
4
5
type P1 = Person["name"];  // string
type P2 = Person["name" | "age"]; // string | number
type P3 = string["charAt"]; // (pos: number) => string
type P4 = string[]["push"]; // (...items: string[]) => number
type P5 = string[][0]; // string

keyof 的作用

JavaScript 是一种高度动态的语言。有时在静态类型系统中捕获某些操作的语义可能会很棘手。以一个简单的 prop 函数为例:

1
2
3
function prop(obj, key) {
return obj[key];
}

该函数接收 obj 和 key 两个参数,并返回对应属性的值。对象上的不同属性,可以具有完全不同的类型,我们甚至不知道 obj 对象长什么样。

那么在 TypeScript 中如何定义上面的 prop 函数呢?我们来尝试一下:

1
2
3
function prop(obj: object, key: string) {
return obj[key];
}

在上面代码中,为了避免调用 prop 函数时传入错误的参数类型,我们为 obj 和 key 参数设置了类型,分别为 {} 和 string 类型。然而,事情并没有那么简单。针对上述的代码,TypeScript 编译器会输出以下错误信息:

1
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{}'.

元素隐式地拥有 any 类型,因为 string 类型不能被用于索引 {} 类型。要解决这个问题,你可以使用以下非常暴力的方案:

1
2
3
function prop(obj: object, key: string) {
return (obj as any)[key];
}

很明显该方案并不是一个好的方案,我们来回顾一下 prop 函数的作用,该函数用于获取某个对象中指定属性的属性值。因此我们期望用户输入的属性是对象上已存在的属性,那么如何限制属性名的范围呢?这时我们可以利用本文的主角 keyof 操作符:

1
2
3
function prop<T extends object, K extends keyof T>(obj: T, key: K) {
return obj[key];
}

在以上代码中,我们使用了 TypeScript 的泛型和泛型约束。首先定义了 T 类型并使用 extends 关键字约束该类型必须是 object 类型的子类型,然后使用 keyof 操作符获取 T 类型的所有键,其返回类型是联合类型,最后利用 extends 关键字约束 K 类型必须为 keyof T 联合类型的子类型。 是骡子是马拉出来遛遛就知道了,我们来实际测试一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type Todo = {
id: number;
text: string;
done: boolean;
}

const todo: Todo = {
id: 1,
text: "Learn TypeScript keyof",
done: false
}

function prop<T extends object, K extends keyof T>(obj: T, key: K) {
return obj[key];
}

const id = prop(todo, "id"); // const id: number
const text = prop(todo, "text"); // const text: string
const done = prop(todo, "done"); // const done: boolean

很明显使用泛型,重新定义后的 prop<T extends object, K extends keyof T>(obj: T, key: K) 函数,已经可以正确地推导出指定键对应的类型。那么当访问 todo 对象上不存在的属性时,会出现什么情况?比如:

1
const date = prop(todo, "date");

对于上述代码,TypeScript 编译器会提示以下错误:

1
Argument of type '"date"' is not assignable to parameter of type '"id" | "text" | "done"'.

这就阻止我们尝试读取不存在的属性。

keyof 与对象的数值属性

在使用对象的数值属性时,我们也可以使用 keyof 关键字。请记住,如果我们定义一个带有数值属性的对象,那么我们既需要定义该属性,又需要使用数组语法访问该属性, 如下所示:

1
2
3
4
5
6
class ClassWithNumericProperty {
[1]: string = "Semlinker";
}

let classWithNumeric = new ClassWithNumericProperty();
console.log(`${classWithNumeric[1]} `);

下面我们来举个示例,介绍一下在含有数值属性的对象中,如何使用 keyof 操作符来安全地访问对象的属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
enum Currency {
CNY = 6,
EUR = 8,
USD = 10
}

const CurrencyName = {
[Currency.CNY]: "人民币",
[Currency.EUR]: "欧元",
[Currency.USD]: "美元"
};

console.log(`CurrencyName[Currency.CNY] = ${CurrencyName[Currency.CNY]}`);
console.log(`CurrencyName[36] = ${CurrencyName[6]}`);

上面的代码中,首先定义了一个 Currency 枚举用于表示三种货币类型,接着定义一个 CurrencyName 对象,该对象使用数值属性作为键,对应的值是该货币类型的名称。该代码成功运行后,控制台会输出以下结果:

1
2
CurrencyName[Currency.CNY] = 人民币
CurrencyName[36] = 人民币

为了方便用户能根据货币类型来获取对应的货币名称,我们来定义一个 getCurrencyName 函数,具体实现如下:

1
2
3
4
5
function getCurrencyName<T, K extends keyof T>(key: K, map: T): T[K] {
return map[key];
}

console.log(`name = ${getCurrencyName(Currency.CNY, CurrencyName)}`);

同样,getCurrencyName 函数和前面介绍的 prop 函数一样,使用了泛型和泛型约束,从而来保证属性的安全访问。最后,我们来简单介绍一下 keyof 与 typeof 操作符如何配合使用。

keyof 与 typeof 操作符

typeof 操作符用于获取变量的类型。因此这个操作符的后面接的始终是一个变量,且需要运用到类型定义当中。为了方便大家理解,我们来举一个具体的示例:

1
2
3
4
5
6
7
8
9
10
11
type Person = {
name: string;
age: number;
}

let man: Person = {
name: "Semlinker",
age: 30
}

type Human = typeof man;

了解完 typeof 和 keyof 操作符的作用,我们来举个例子,介绍一下它们如何结合在一起使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const COLORS = {
red: 'red',
blue: 'blue'
}

// 首先通过typeof操作符获取color变量的类型,然后通过keyof操作符获取该类型的所有键,
// 即字符串字面量联合类型 'red' | 'blue'
type Colors = keyof typeof COLORS
let color: Colors;
color = 'red' // Ok
color = 'blue' // Ok

// Type '"yellow"' is not assignable to type '"red" | "blue"'.
color = 'yellow' // Error

六、TypeScript 泛型

软件工程中,我们不仅要创建一致的定义良好的 API,同时也要考虑可重用性。 组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这在创建大型系统时为你提供了十分灵活的功能。

在像 C#Java 这样的语言中,可以使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据。 这样用户就可以以自己的数据类型来使用组件。

设计泛型的关键目的是在成员之间提供有意义的约束,这些成员可以是:类的实例成员、类的方法、函数参数和函数返回值。

泛型(Generics)是允许同一个函数接受不同类型参数的一种模板。相比于使用 any 类型,使用泛型来创建可复用的组件要更好,因为泛型会保留参数类型。

泛型接口

1
2
3
interface GenericIdentityFn<T> {
(arg: T): T;
}

泛型类

1
2
3
4
5
6
7
8
9
10
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) {
return x + y;
};

泛型变量

对刚接触 TypeScript 泛型的小伙伴来说,看到 T 和 E,还有 K 和 V 这些泛型变量时,估计会一脸懵逼。其实这些大写字母并没有什么本质的区别,只不过是一个约定好的规范而已。也就是说使用大写字母 A-Z 定义的类型变量都属于泛型,把 T 换成 A,也是一样的。下面我们介绍一下一些常见泛型变量代表的意思:

  • T(Type):表示一个 TypeScript 类型
  • K(Key):表示对象中的键类型
  • V(Value):表示对象中的值类型
  • E(Element):表示元素类型

泛型工具类型

为了方便开发者 TypeScript 内置了一些常用的工具类型,比如 Partial、Required、Readonly、Record 和 ReturnType 等。出于篇幅考虑,这里我们只简单介绍 Partial 工具类型。不过在具体介绍之前,我们得先介绍一些相关的基础知识,方便读者自行学习其它的工具类型。

1.typeof
在 TypeScript 中,typeof 操作符可以用来获取一个变量声明或对象的类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
interface Person {
name: string;
age: number;
}

const sem: Person = { name: 'semlinker', age: 30 };
type Sem= typeof sem; // -> Person

function toArray(x: number): Array<number> {
return [x];
}

type Func = typeof toArray; // -> (x: number) => number[]

2.keyof
keyof 操作符可以用来获取一个对象中的所有 key 值:

1
2
3
4
5
6
7
8
interface Person {
name: string;
age: number;
}

type K1 = keyof Person; // "name" | "age"
type K2 = keyof Person[]; // "length" | "toString" | "pop" | "push" | "concat" | "join"
type K3 = keyof { [x: string]: Person }; // string | number

3.in
in 用来遍历枚举类型:

1
2
3
4
5
type Keys = "a" | "b" | "c"

type Obj = {
[p in Keys]: any
} // -> { a: any, b: any, c: any }

4.infer
在条件类型语句中,可以用 infer 声明一个类型变量并且对它进行使用。

1
2
3
type ReturnType<T> = T extends (
...args: any[]
) => infer R ? R : any;

以上代码中 infer R 就是声明一个变量来承载传入函数签名的返回值类型,简单说就是用它取到函数返回值的类型方便之后使用。

5.extends
有时候我们定义的泛型不想过于灵活或者说想继承某些类等,可以通过 extends 关键字添加泛型约束。

1
2
3
4
5
6
7
8
interface ILengthwise {
length: number;
}

function loggingIdentity<T extends ILengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}

现在这个泛型函数被定义了约束,因此它不再是适用于任意类型:

1
loggingIdentity(3);  // Error, number doesn't have a .length property

这时我们需要传入符合约束类型的值,必须包含必须的属性:

1
loggingIdentity({length: 10, value: 3});

6.Partial

Partial 的作用就是将某个类型里的属性全部变为可选项 ?。

定义:

1
2
3
4
5
6
7
/**
* node_modules/typescript/lib/lib.es5.d.ts
* Make all properties in T optional
*/
type Partial<T> = {
[P in keyof T]?: T[P];
};

在以上代码中,首先通过 keyof T 拿到 T 的所有属性名,然后使用 in 进行遍历,将值赋给 P,最后通过 T[P] 取得相应的属性值。中间的 ? 号,用于将所有属性变为可选。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface Todo {
title: string;
description: string;
}

function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) {
return { ...todo, ...fieldsToUpdate };
}

const todo1 = {
title: "organize desk",
description: "clear clutter",
};

const todo2 = updateTodo(todo1, {
description: "throw out trash",
});

在上面的 updateTodo 方法中,我们利用 Partial 工具类型,定义 fieldsToUpdate 的类型为 Partial,即:

1
2
3
4
{
title?: string | undefined;
description?: string | undefined;
}

七、充分利用泛型和类型运算避免冗余类型标记

使用泛型提取公共的 util type,简化类型编写

1
2
3
4
5
6
7
8
9
interface ButtonProps {
type: string;
size: 'large' | 'middle'| 'small'
}
interface ButtonPropsWithChildren{
type: string;
size: 'large' | 'middle'| 'small',
children: React.ReactNode
}

使用 PropsWithChildren 简化

1
2
import { PropsWithChildren } from 'react';
type ButtonPropsWithChildren = PropsWithChildren<ButtonProps>

使用 index type | mapped type | keyof 等进行类型传递

1
2
3
4
5
6
7
8
9
10
11
interface State {
userId: string;
pageTitle: string;
recentFiles: string[]
pageContents: string;
}
interface TopNavState {
userId: string;
pageTitle: string;
recentFiles: string[]
}

上述代码可通过 lookup type 简化

1
2
3
4
5
interface TopNavState = {
userId: State['userId'];
pageTitle: State['pageTitle']
recentFiles: State['recentFiles']
}

使用 mapped type 可进一步简化

1
2
3
type TopNavState = {
[k in 'userId' | 'pageTitle' | 'recentFiles'] : State[k]
}

再使用工具类进一步简化

1
type TopNavState = Pick<State, 'userId', 'pageTitle', 'rencentFiles'>

我们也可以利用 typeof 来进行类型传递

1
2
3
4
5
6
7
8
9
10
11
function getUserInfo(userId:string){
return {
userId,
name,
age,
height,
weight,
favoriteColor
}
}
type UserInfo = ReturnType<typeof getUserInfo>

编写 utility type 时,多多使用 generic constraint 保证实例化时的类型安全

1
2
3
4
5
6
7
8
9
10
11
12
13
interface Name {
first: string;
last: string
}
type Pick1<T, K>{
[k in K]: T[k]
}
type FirstLast = Pick1<Name, 'first'| 'last'>
type FirstMiddle = Pick1<Name, 'first', 'middle'> // 应该报错但没报错
type Pick2<T, K extends keyof T> = { // 添加泛型约束
[k in K]: T[K]
}
type FirstMiddle = Pick2<Name, 'first', 'middle'> // 正确的报错了

八、用 TypeScript 编写 React 的最佳实践

组件

React 的核心概念之一是组件。在这里,我们将引用 React v16.8 以后的标准组件,这意味着使用 Hook 而不是类的组件。

通常,一个基本的组件有很多需要关注的地方。让我们看一个例子:

1
2
3
4
5
6
7
8
9
import React from 'react'

// 函数声明式写法
function Heading(): React.ReactNode {
  return <h1>My Website Heading</h1>
}

// 函数扩展式写法
const OtherHeading: React.FC = () => <h1>My Website Heading</h1>

注意这里的关键区别。在第一个例子中,我们使用函数声明式写法,我们注明了这个函数返回值是 React.ReactNode 类型。相反,第二个例子使用了一个函数表达式。因为第二个实例返回一个函数,而不是一个值或表达式,所以我们我们注明了这个函数返回值是 React.FC 类型。

记住这两种方式可能会让人混淆。这主要取决于设计选择。无论您选择在项目中使用哪个,都要始终如一地使用它。

Props

我们将介绍的下一个核心概念是 Props。你可以使用 interface 或 type 来定义 Props 。让我们看另一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React from 'react'

interface Props {
  name: string;
  color: string;
}

type OtherProps = {
  name: string;
  color: string;
}

// Notice here we're using the function declaration with the interface Props
function Heading({ name, color }: Props): React.ReactNode {
  return <h1>My Website Heading</h1>
}

// Notice here we're using the function expression with the type OtherProps
const OtherHeading: React.FC<OtherProps> = ({ name, color }) =>
  <h1>My Website Heading</h1>

关于 interface 或 type ,我们建议遵循 react-typescript-cheatsheet 社区提出的准则:

  • 在编写库或第三方环境类型定义时,始终将 interface 用于公共 API 的定义。
  • 考虑为你的 React 组件的 State 和 Props 使用 type ,因为它更受约束。”

让我们再看一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React from 'react'

type Props = {
   /** color to use for the background */
  color?: string;
   /** standard children prop: accepts any valid React Node */
  children: React.ReactNode;
   /** callback function passed to the onClick handler*/
  onClick: ()  => void;
}

const Button: React.FC<Props> = ({ children, color = 'tomato', onClick }) => {
   return <button style={{ backgroundColor: color }} onClick={onClick}>{children}</button>
}

在此 <Button /> 组件中,我们为 Props 使用 type。每个 Props 上方都有简短的说明,以为其他开发人员提供更多背景信息。? 表示 Props 是可选的。children props 是一个 React.ReactNode 表示它还是一个 React 组件。

通常,在 React 和 TypeScript 项目中编写 Props 时,请记住以下几点:

  • 始终使用 TSDoc 标记为你的 Props 添加描述性注释 /** comment */
  • 无论你为组件 Props 使用 type 还是 interface ,都应始终使用它们。
  • 如果 props 是可选的,请适当处理或使用默认值。

Hooks

幸运的是,当使用 Hook 时, TypeScript 类型推断工作得很好。这意味着你没有什么好担心的。举个例子:

1
2
3
// `value` is inferred as a string
// `setValue` is inferred as (newValue: string) => void
const [value, setValue] = useState('')

TypeScript 推断出 useState 钩子给出的值。这是一个 React 和 TypeScript 协同工作的成果。

在极少数情况下,你需要使用一个空值初始化 Hook ,可以使用泛型并传递联合以正确键入 Hook 。查看此实例:

1
2
3
4
5
6
7
8
9
type User = {
  email: string;
  id: string;
}

// the generic is the < >
// the union is the User | null
// together, TypeScript knows, "Ah, user can be User or null".
const [user, setUser] = useState<User | null>(null);

下面是一个使用 useReducer 的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type AppState = {};
type Action =
  | { type: "SET_ONE"; payload: string }
  | { type: "SET_TWO"; payload: number };

export function reducer(state: AppState, action: Action): AppState {
  switch (action.type) {
    case "SET_ONE":
      return {
        ...state,
        one: action.payload // `payload` is string
      };
    case "SET_TWO":
      return {
        ...state,
        two: action.payload // `payload` is number
      };
    default:
      return state;
  }
}

可见,Hooks 并没有为 React 和 TypeScript 项目增加太多复杂性。

常见用例

处理表单事件

最常见的情况之一是 onChange 在表单的输入字段上正确键入使用的。这是一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
import React from 'react'

const MyInput = () => {
  const [value, setValue] = React.useState('')

  // 事件类型是“ChangeEvent”
  // 我们将 “HTMLInputElement” 传递给 input
  function onChange(e: React.ChangeEvent<HTMLInputElement>) {
    setValue(e.target.value)
  }

  return <input value={value} onChange={onChange} id="input-example"/>
}

扩展组件的 Props

有时,你希望获取为一个组件声明的 Props,并对它们进行扩展,以便在另一个组件上使用它们。但是你可能想要修改一两个属性。还记得我们如何看待两种类型组件 Props、type 或 interface 的方法吗?取决于你使用的组件决定了你如何扩展组件 Props 。让我们先看看如何使用 type:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from 'react';

type ButtonProps = {
    /** the background color of the button */
    color: string;
    /** the text to show inside the button */
    text: string;
}

type ContainerProps = ButtonProps & {
    /** the height of the container (value used with 'px') */
    height: number;
}

const Container: React.FC<ContainerProps> = ({ color, height, width, text }) => {
  return <div style={{ backgroundColor: color, height: `${height}px` }}>{text}</div>
}

如果你使用 interface 来声明 props,那么我们可以使用关键字 extends 从本质上“扩展”该接口,但要进行一些修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from 'react';

interface ButtonProps {
    /** the background color of the button */
    color: string;
    /** the text to show inside the button */
    text: string;
}

interface ContainerProps extends ButtonProps {
    /** the height of the container (value used with 'px') */
    height: number;
}

const Container: React.FC<ContainerProps> = ({ color, height, width, text }) => {
  return <div style={{ backgroundColor: color, height: `${height}px` }}>{text}</div>
}

两种方法都可以解决问题。由您决定使用哪个。就个人而言,扩展 interface 更具可读性,但最终取决于你和你的团队。

九、编译上下文

tsconfig.json 的作用

  • 用于标识 TypeScript 项目的根路径;
  • 用于配置 TypeScript 编译器;
  • 用于指定编译的文件。

tsconfig.json 重要字段

  • files - 设置要编译的文件的名称;
  • include - 设置需要进行编译的文件,支持路径模式匹配;
  • exclude - 设置无需进行编译的文件,支持路径模式匹配;
  • compilerOptions - 设置与编译流程相关的选项。

compilerOptions 选项

compilerOptions 支持很多选项,常见的有 baseUrl、 target、baseUrl、 moduleResolution 和 lib 等。

compilerOptions 每个选项的详细说明如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
{
"compilerOptions": {

/* 基本选项 */
"target": "es5", // 指定 ECMAScript 目标版本: 'ES3' (default), 'ES5', 'ES6'/'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'
"module": "commonjs", // 指定使用模块: 'commonjs', 'amd', 'system', 'umd' or 'es2015'
"lib": [], // 指定要包含在编译中的库文件
"allowJs": true, // 允许编译 javascript 文件
"checkJs": true, // 报告 javascript 文件中的错误
"jsx": "preserve", // 指定 jsx 代码的生成: 'preserve', 'react-native', or 'react'
"declaration": true, // 生成相应的 '.d.ts' 文件
"sourceMap": true, // 生成相应的 '.map' 文件
"outFile": "./", // 将输出文件合并为一个文件
"outDir": "./", // 指定输出目录
"rootDir": "./", // 用来控制输出目录结构 --outDir.
"removeComments": true, // 删除编译后的所有的注释
"noEmit": true, // 不生成输出文件
"importHelpers": true, // 从 tslib 导入辅助工具函数
"isolatedModules": true, // 将每个文件做为单独的模块 (与 'ts.transpileModule' 类似).

/* 严格的类型检查选项 */
"strict": true, // 启用所有严格类型检查选项
"noImplicitAny": true, // 在表达式和声明上有隐含的 any类型时报错
"strictNullChecks": true, // 启用严格的 null 检查
"noImplicitThis": true, // 当 this 表达式值为 any 类型的时候,生成一个错误
"alwaysStrict": true, // 以严格模式检查每个模块,并在每个文件里加入 'use strict'

/* 额外的检查 */
"noUnusedLocals": true, // 有未使用的变量时,抛出错误
"noUnusedParameters": true, // 有未使用的参数时,抛出错误
"noImplicitReturns": true, // 并不是所有函数里的代码都有返回值时,抛出错误
"noFallthroughCasesInSwitch": true, // 报告 switch 语句的 fallthrough 错误。(即,不允许 switch 的 case 语句贯穿)

/* 模块解析选项 */
"moduleResolution": "node", // 选择模块解析策略: 'node' (Node.js) or 'classic' (TypeScript pre-1.6)
"baseUrl": "./", // 用于解析非相对模块名称的基目录
"paths": {}, // 模块名到基于 baseUrl 的路径映射的列表
"rootDirs": [], // 根文件夹列表,其组合内容表示项目运行时的结构内容
"typeRoots": [], // 包含类型声明的文件列表
"types": [], // 需要包含的类型声明文件名列表
"allowSyntheticDefaultImports": true, // 允许从没有设置默认导出的模块中默认导入。

/* Source Map Options */
"sourceRoot": "./", // 指定调试器应该找到 TypeScript 文件而不是源文件的位置
"mapRoot": "./", // 指定调试器应该找到映射文件而不是生成文件的位置
"inlineSourceMap": true, // 生成单个 soucemaps 文件,而不是将 sourcemaps 生成不同的文件
"inlineSources": true, // 将代码与 sourcemaps 生成到一个文件中,要求同时设置了 --inlineSourceMap 或 --sourceMap 属性

/* 其他选项 */
"experimentalDecorators": true, // 启用装饰器
"emitDecoratorMetadata": true // 为装饰器提供元数据的支持
}
}

tsconfig.json 配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
{
  "compilerOptions": {
    "target": "es5", // 指定 ECMAScript 版本
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ], // 要包含在编译中的依赖库文件列表
    "allowJs": true, // 允许编译 JavaScript 文件
    "skipLibCheck": true, // 跳过所有声明文件的类型检查
    "esModuleInterop": true, // 禁用命名空间引用 (import * as fs from "fs") 启用 CJS/AMD/UMD 风格引用 (import fs from "fs")
    "allowSyntheticDefaultImports": true, // 允许从没有默认导出的模块进行默认导入
    "strict": true, // 启用所有严格类型检查选项
    "forceConsistentCasingInFileNames": true, // 不允许对同一个文件使用不一致格式的引用
    "module": "esnext", // 指定模块代码生成
    "moduleResolution": "node", // 使用 Node.js 风格解析模块
    "resolveJsonModule": true, // 允许使用 .json 扩展名导入的模块
    "noEmit": true, // 不输出(意思是不编译代码,只执行类型检查)
    "jsx": "react", // 在.tsx文件中支持JSX
    "sourceMap": true, // 生成相应的.map文件
    "declaration": true, // 生成相应的.d.ts文件
    "noUnusedLocals": true, // 报告未使用的本地变量的错误
    "noUnusedParameters": true, // 报告未使用参数的错误
    "experimentalDecorators": true, // 启用对ES装饰器的实验性支持
    "incremental": true, // 通过从以前的编译中读取/写入信息到磁盘上的文件来启用增量编译
    "noFallthroughCasesInSwitch": true 
  },
  "include": [
    "src/**/*" // *** TypeScript文件应该进行类型检查 ***
  ],
  "exclude": ["node_modules", "build"] // *** 不进行类型检查的文件 ***
}

十、参考资料

Vue2 每次都把整个 Vue 导入,例如 Vue2 的 main.js 文件中的代码

1
2
3
4
5
6
7
8
9
import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

new Vue({
render: h => h(App)
}).$mount('#app')

但很明显我们的项目中不可能用到 Vue 所有的 API,因此很多模块其实是没有用的

那么在 Vue3 中,对外暴露了很多的 API 供开发者使用,我们可以根据自己的需求,将所需要的 API 从 Vue 中导入。例如 main.js 中的代码

1
2
3
4
import { createApp } from 'vue';
import App from './App.vue'

createApp(App).mount('#app')

利用了 import 和 export 的导入导出语法,实现了按需打包模块的功能,项目打包后的文件体积明显小了很多

这也是我们本文需要对 Vue3 API 进行详细了解的原因

(1)setup

setup 函数也是 Composition API 的入口函数,我们的变量、方法都是在该函数里定义的,来看一下使用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<template>
<div id="app">
<p>{{ number }}</p>
<button @click="add">增加</button>
</div>
</template>

<script>
// 1. 从 vue 中引入 ref 函数
import {ref} from 'vue'
export default {
name: 'App',
setup() {
// 2. 用 ref 函数包装一个响应式变量 number
let number = ref(0)

// 3. 设定一个方法
function add() {
// number是被ref函数包装过了的,其值保存在.value中
number.value ++
}

// 4. 将 number 和 add 返回出去,供template中使用
return {number, add}
}

}
</script>

上述代码中用到了 ref 函数,下面会详细讲解,在这里你只需要理解它的作用是包装一个响应式的数据即可,并且你可以将 ref 函数包装过的变量看作是 Vue2 data 中的变量

这样就简单实现了一个点击按钮数字加 1 的功能


在 Vue2 中,我们访问 data 或 props 中的变量,都是通过类似 this.number 这样的形式去获取的,但要特别注意的是,在 setup 中,this 指向的是 undefined,也就是说不能再向 Vue2 一样通过 this 去获取变量了

那么到底该如何获取到 props 中的数据呢?

其实 setup 函数还有两个参数,分别是 props 、context,前者存储着定义当前组件允许外界传递过来的参数名称以及对应的值;后者是一个上下文对象,能从中访问到 attr 、emit 、slots

其中 emit 就是我们熟悉的 Vue2 中与父组件通信的方法,可以直接拿来调用

(2)生命周期

Vue2 中有 beforeCreate 、created 、beforeMount 、mounted 、beforeUpdate 等生命周期函数

而在 Vue3 中,这些生命周期部分有所变化,并且调用的方式也有所改变,下面放上一张变化图来简单了解一下

Vue2 Vue3
beforeCreate setup
created setup
beforeMount onBeforeMount
mounted onMounted
beforeUpdate onBeforeUpdate
updated onUpdated
beforeDestory onBeforeUnmount
destoryed onUnmounted

Vue3 的这些生命周期调用也很简单,同样是先从 vue 中导入,再进行直接调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<template>
<div id="app"></div>
</template>

<script>
// 1. 从 vue 中引入 多个生命周期函数
import {onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, unMounted} from 'vue'
export default {
name: 'App',
setup() {
onBeforeMount(() => {
// 在挂载前执行某些代码
})

onMounted(() => {
// 在挂载后执行某些代码
})

onBeforeUpdate(() => {
// 在更新前前执行某些代码
})

onUpdated(() => {
// 在更新后执行某些代码
})

onBeforeUnmount(() => {
// 在组件销毁前执行某些代码
})

onUnmounted(() => {
// 在组件销毁后执行某些代码
})

return {}
}

}
</script>

(3)reactive

reactive 方法是用来创建一个响应式的数据对象,该 API 也很好地解决了 Vue2 通过 defineProperty 实现数据响应式的缺陷

用法很简单,只需将数据作为参数传入即可,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<div id="app">
<!-- 4. 访问响应式数据对象中的 count -->
{{ state.count }}
</div>
</template>

<script>
// 1. 从 vue 中导入 reactive
import {reactive} from 'vue'
export default {
name: 'App',
setup() {
// 2. 创建响应式的数据对象
const state = reactive({count: 3})

// 3. 将响应式数据对象state return 出去,供template使用
return {state}
}
}
</script>

(4)ref

在介绍 setup 函数时,我们使用了 ref 函数包装了一个响应式的数据对象,这里表面上看上去跟 reactive 好像功能一模一样啊,确实差不多,因为 ref 就是通过 reactive 包装了一个对象 ,然后是将值传给该对象中的 value 属性,这也就解释了为什么每次访问时我们都需要加上 .value

我们可以简单地把 ref(obj) 理解为这个样子 reactive({value: obj})

这里我们写一段代码来具体看一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script>
import {ref, reactive} from 'vue'
export default {
name: 'App',
setup() {
const obj = {count: 3}
const state1 = ref(obj)
const state2 = reactive(obj)

console.log(state1)
console.log(state2)
}

}
</script>

来看一下打印结果
image

注意: 这里指的 .value 是在 setup 函数中访问 ref 包装后的对象时才需要加的,在 template 模板中访问时是不需要的,因为在编译时,会自动识别其是否为 ref 包装过的

那么我们到底该如何选择 ref 和 reactive 呢?

建议:

  1. 基本类型值(String 、Number 、Boolean 等)或单值对象(类似像 {count: 3} 这样只有一个属性值的对象)使用 ref
  2. 引用类型值(Object 、Array)使用 reactive

(5)toRef

toRef 是将某个对象中的某个值转化为响应式数据,其接收两个参数,第一个参数为 obj 对象;第二个参数为对象中的属性名

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script>
// 1. 导入 toRef
import {toRef} from 'vue'
export default {
setup() {
const obj = {count: 3}
// 2. 将 obj 对象中属性count的值转化为响应式数据
const state = toRef(obj, 'count')

// 3. 将toRef包装过的数据对象返回供template使用
return {state}
}
}
</script>

但其实表面上看上去 toRef 这个 API 好像非常的没用,因为这个功能也可以用 ref 实现,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script>
// 1. 导入 ref
import {ref} from 'vue'
export default {
setup() {
const obj = {count: 3}
// 2. 将 obj 对象中属性count的值转化为响应式数据
const state = ref(obj.count)

// 3. 将ref包装过的数据对象返回供template使用
return {state}
}
}
</script>

乍一看好像还真是,其实这两者是有区别的,我们可以通过一个案例来比较一下,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<template>
<p>{{ state1 }}</p>
<button @click="add1">增加</button>

<p>{{ state2 }}</p>
<button @click="add2">增加</button>
</template>

<script>
import {ref, toRef} from 'vue'
export default {
setup() {
const obj = {count: 3}
const state1 = ref(obj.count)
const state2 = toRef(obj, 'count')

function add1() {
state1.value ++
console.log('原始值:', obj);
console.log('响应式数据对象:', state1);
}

function add2() {
state2.value ++
console.log('原始值:', obj);
console.log('响应式数据对象:', state2);
}

return {state1, state2, add1, add2}
}
}
</script>

我们分别用 ref 和 toRef 将 obj 中的 count 转化为响应式,并声明了两个方法分别使 count 值增加,每次增加后打印一下原始值 obj 和被包装过的响应式数据对象,同时还要看看视图的变化

ref:

image

可以看到,在对响应式数据的值进行 +1 操作后,视图改变了,原始值未改变,响应式数据对象的值也改变了,这说明 ref 是对原数据的一个拷贝,不会影响到原始值,同时响应式数据对象值改变后会同步更新视图

toRef:

image

可以看到,在对响应式数据的值进行 +1 操作后,视图未发生改变,原始值改变了,响应式数据对象的值也改变了,这说明 toRef 是对原数据的一个引用,会影响到原始值,但是响应式数据对象值改变后会不会更新视图

总结:

  • ref 是对传入数据的拷贝;toRef 是对传入数据的引用
  • ref 的值改变会更新视图;toRef 的值改变不会更新视图

(6)toRefs

了解完 toRef 后,就很好理解 toRefs 了,其作用就是将传入的对象里所有的属性的值都转化为响应式数据对象,该函数支持一个参数,即 obj 对象

我们来看一下它的基本使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script>
// 1. 导入 toRefs
import {toRefs} from 'vue'
export default {
setup() {
const obj = {
name: '前端印象',
age: 22,
gender: 0
}
// 2. 将 obj 对象中属性count的值转化为响应式数据
const state = toRefs(obj)

// 3. 打印查看一下
console.log(state)
}
}
</script>

打印结果如下:
image

返回的是一个对象,对象里包含了每一个包装过后的响应式数据对象

(7)shallowReactive

听这个 API 的名称就知道,这是一个渐层的 reactive,难道意思就是原本的 reactive 是深层的呗,没错,这是一个用于性能优化的 API

其实将 obj 作为参数传递给 reactive 生成响应式数据对象时,若 obj 的层级不止一层,那么会将每一层都用 Proxy 包装一次,我们来验证一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script>
import {reactive} from 'vue'
export default {
setup() {
const obj = {
a: 1,
first: {
b: 2,
second: {
c: 3
}
}
}

const state = reactive(obj)

console.log(state)
console.log(state.first)
console.log(state.first.second)
}
}
</script>

来看一下打印结果:

image

设想一下如果一个对象层级比较深,那么每一层都用 Proxy 包装后,对于性能是非常不友好的

接下来我们再来看看 shallowReactive

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script>
import {shallowReactive} from 'vue'
export default {
setup() {
const obj = {
a: 1,
first: {
b: 2,
second: {
c: 3
}
}
}

const state = shallowReactive(obj)

console.log(state)
console.log(state.first)
console.log(state.first.second)
}
}
</script>

来看一下打印结果:

image

结果非常的明了了,只有第一层被 Proxy 处理了,也就是说只有修改第一层的值时,才会响应式更新,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<template>
<p>{{ state.a }}</p>
<p>{{ state.first.b }}</p>
<p>{{ state.first.second.c }}</p>
<button @click="change1">改变1</button>
<button @click="change2">改变2</button>
</template>
<script>
import {shallowReactive} from 'vue'
export default {
setup() {
const obj = {
a: 1,
first: {
b: 2,
second: {
c: 3
}
}
}

const state = shallowReactive(obj)

function change1() {
state.a = 7
}

function change2() {
state.first.b = 8
state.first.second.c = 9
console.log(state);
}

return {state}
}
}
</script>

来看一下具体过程:

image

首先我们点击了第二个按钮,改变了第二层的 b 和第三层的 c,虽然值发生了改变,但是视图却没有进行更新;

当我们点击了第一个按钮,改变了第一层的 a 时,整个视图进行了更新;

由此可说明,shallowReactive 监听了第一层属性的值,一旦发生改变,则更新视图

(8)shallowRef

这是一个浅层的 ref,与 shallowReactive 一样是拿来做性能优化的

shallowReactive 是监听对象第一层的数据变化用于驱动视图更新,那么 shallowRef 则是监听 .value 的值的变化来更新视图的

我们来看一下具体代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<template>
<p>{{ state.a }}</p>
<p>{{ state.first.b }}</p>
<p>{{ state.first.second.c }}</p>
<button @click="change1">改变1</button>
<button @click="change2">改变2</button>
</template>

<script>
import {shallowRef} from 'vue'
export default {
setup() {
const obj = {
a: 1,
first: {
b: 2,
second: {
c: 3
}
}
}

const state = shallowRef(obj)
console.log(state);

function change1() {
// 直接将state.value重新赋值
state.value = {
a: 7,
first: {
b: 8,
second: {
c: 9
}
}
}
}

function change2() {
state.value.first.b = 8
state.value.first.second.c = 9
console.log(state);
}

return {state, change1, change2}
}
}
</script>

首先看一下被 shallowRef 包装过后是怎样的结构

image

然后再来看看改变其值会有什么变化

image

我们先点击了第二个按钮,发现数据确实被改变了,但是视图并没随之更新;

于是点击了第一个按钮,即将整个 .value 重新赋值了,视图就立马更新了

这么一看,未免也太过麻烦了,改个数据还要重新赋值,不要担心,此时我们可以用到另一个 API,叫做 triggerRef ,调用它就可以立马更新视图,其接收一个参数 state ,即需要更新的 ref 对象

我们来使用一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<template>
<p>{{ state.a }}</p>
<p>{{ state.first.b }}</p>
<p>{{ state.first.second.c }}</p>
<button @click="change">改变</button>
</template>

<script>
import {shallowRef, triggerRef} from 'vue'
export default {
setup() {
const obj = {
a: 1,
first: {
b: 2,
second: {
c: 3
}
}
}

const state = shallowRef(obj)
console.log(state);

function change() {
state.value.first.b = 8
state.value.first.second.c = 9
// 修改值后立即驱动视图更新
triggerRef(state)
console.log(state);
}

return {state, change}
}
}
</script>

我们来看一下具体过程
image

可以看到,我们没有给 .value 重新赋值,只是在修改值后,调用了 triggerRef 就实现了视图的更新

(9)toRaw

toRaw 方法是用于获取 ref 或 reactive 对象的原始数据的

先来看一段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<template>
<p>{{ state.name }}</p>
<p>{{ state.age }}</p>
<button @click="change">改变</button>
</template>

<script>
import {reactive} from 'vue'
export default {
setup() {
const obj = {
name: '前端印象',
age: 22
}

const state = reactive(obj)

function change() {
state.age = 90
console.log(obj); // 打印原始数据obj
console.log(state); // 打印 reactive对象
}

return {state, change}
}
}
</script>

来看看具体过程

image

我们改变了 reactive 对象中的数据,于是看到原始数据 obj 和被 reactive 包装过的对象的值都发生了变化,由此我们可以看出,这两者是一个引用关系

那么此时我们就想了,那如果直接改变原始数据 obj 的值,会怎么样呢?答案是: reactive 的值也会跟着改变,但是视图不会更新

由此可见,当我们想修改数据,但不想让视图更新时,可以选择直接修改原始数据上的值,因此需要先获取到原始数据,我们可以使用 Vue3 提供的 toRaw 方法

toRaw 接收一个参数,即 ref 对象或 reactive 对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script>
import {reactive, toRaw} from 'vue'
export default {
setup() {
const obj = {
name: '前端印象',
age: 22
}

const state = reactive(obj)
const raw = toRaw(state)

console.log(obj === raw) // true
}
}
</script>

上述代码就证明了 toRaw 方法从 reactive 对象中获取到的是原始数据,因此我们就可以很方便的通过修改原始数据的值而不更新视图来做一些性能优化了

注意: 补充一句,当 toRaw 方法接收的参数是 ref 对象时,需要加上 .value 才能获取到原始数据对象

(10)markRaw

markRaw 方法可以将原始数据标记为非响应式的,即使用 ref 或 reactive 将其包装,仍无法实现数据响应式,其接收一个参数,即原始数据,并返回被标记后的数据

我们来看一下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<template>
<p>{{ state.name }}</p>
<p>{{ state.age }}</p>
<button @click="change">改变</button>
</template>

<script>
import {reactive, markRaw} from 'vue'
export default {
setup() {
const obj = {
name: '前端印象',
age: 22
}
// 通过markRaw标记原始数据obj, 使其数据更新不再被追踪
const raw = markRaw(obj)
// 试图用reactive包装raw, 使其变成响应式数据
const state = reactive(raw)

function change() {
state.age = 90
console.log(state);
}

return {state, change}
}
}
</script>

我们来看一下在被 markRaw 方法处理过后的数据是否还能被 reactive 包装成响应式数据

image

从图中可以看到,即使我们修改了值也不会更新视图了,即没有实现数据响应式

(11)provide && inject

与 Vue2 中的 provide 和 inject 作用相同,只不过在 Vue3 中需要手动从 vue 中导入

这里简单说明一下这两个方法的作用:

  • provide :向子组件以及子孙组件传递数据。接收两个参数,第一个参数是 key,即数据的名称;第二个参数为 value,即数据的值
  • inject :接收父组件或祖先组件传递过来的数据。接收一个参数 key,即父组件或祖先组件传递的数据名称

假设这有三个组件,分别是 A.vue 、B.vue 、C.vue,其中 B.vue 是 A.vue 的子组件,C.vue 是 B.vue 的子组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// A.vue
<script>
import {provide} from 'vue'
export default {
setup() {
const obj= {
name: '前端印象',
age: 22
}

// 向子组件以及子孙组件传递名为info的数据
provide('info', obj)
}
}
</script>

// B.vue
<script>
import {inject} from 'vue'
export default {
setup() {
// 接收A.vue传递过来的数据
inject('info') // {name: '前端印象', age: 22}
}
}
</script>

// C.vue
<script>
import {inject} from 'vue'
export default {
setup() {
// 接收A.vue传递过来的数据
inject('info') // {name: '前端印象', age: 22}
}
}
</script>

(12)watch && watchEffect

watch 和 watchEffect 都是用来监视某项数据变化从而执行指定的操作的,但用法上还是有所区别

watch:watch( source, cb, [options] )

参数说明:

  • source:可以是表达式或函数,用于指定监听的依赖对象
  • cb:依赖对象变化后执行的回调函数
  • options:可选参数,可以配置的属性有 immediate(立即触发回调函数)、deep(深度监听)
    当监听 ref 类型时:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script>
import {ref, watch} from 'vue'
export default {
setup() {
const state = ref(0)

watch(state, (newValue, oldValue) => {
console.log(`原值为${oldValue}`)
console.log(`新值为${newValue}`)
/* 1秒后打印结果:
原值为0
新值为1
*/
})

// 1秒后将state值+1
setTimeout(() => {
state.value ++
}, 1000)
}
}
</script>

当监听 reactive 类型时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script>
import {reactive, watch} from 'vue'
export default {
setup() {
const state = reactive({count: 0})

watch(() => state.count, (newValue, oldValue) => {
console.log(`原值为${oldValue}`)
console.log(`新值为${newValue}`)
/* 1秒后打印结果:
原值为0
新值为1
*/
})

// 1秒后将state.count的值+1
setTimeout(() => {
state.count ++
}, 1000)
}
}
</script>

当同时监听多个值时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<script>
import {reactive, watch} from 'vue'
export default {
setup() {
const state = reactive({ count: 0, name: 'zs' })

watch(
[() => state.count, () => state.name],
([newCount, newName], [oldvCount, oldvName]) => {
console.log(oldvCount) // 旧的 count 值
console.log(newCount) // 新的 count 值
console.log(oldName) // 旧的 name 值
console.log(newvName) // 新的 name 值
}
)

setTimeout(() => {
state.count ++
state.name = 'ls'
}, 1000)
}
}
</script>

因为 watch 方法的第一个参数我们已经指定了监听的对象,因此当组件初始化时,不会执行第二个参数中的回调函数,若我们想让其初始化时就先执行一遍,可以在第三个参数对象中设置 immediate: true

watch 方法默认是浅层的监听我们指定的数据,例如如果监听的数据有多层嵌套,深层的数据变化不会触发监听的回调,若我们想要其对深层数据也进行监听,可以在第三个参数对象中设置 deep: true

补充: watch 方法会返回一个 stop 方法,若想要停止监听,便可直接执行该 stop 函数

接下来再来聊聊 watchEffect,它与 watch 的区别主要有以下几点:

  1. 不需要手动传入依赖
  2. 每次初始化时会执行一次回调函数来自动获取依赖
  3. 无法获取到原值,只能得到变化后的值
    来看一下该方法如何使用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<script>
import {reactive, watchEffect} from 'vue'
export default {
setup() {
const state = reactive({ count: 0, name: 'zs' })

watchEffect(() => {
console.log(state.count)
console.log(state.name)
/* 初始化时打印:
0
zs

1秒后打印:
1
ls
*/
})

setTimeout(() => {
state.count ++
state.name = 'ls'
}, 1000)
}
}
</script>

从上述代码中可以看出,我们并没有像 watch 方法一样先给其传入一个依赖,而是直接指定了一个回调函数

当组件初始化时,将该回调函数执行一次,自动获取到需要检测的数据是 state.count 和 state.name

根据以上特征,我们可以自行选择使用哪一个监听器

(13)getCurrentInstance

我们都知道在 Vue2 的任何一个组件中想要获取当前组件的实例可以通过 this 来得到,而在 Vue3 中我们大量的代码都在 setup 函数中运行,并且在该函数中 this 指向的是 undefined,那么该如何获取到当前组件的实例呢?

这时可以用到另一个方法,即 getCurrentInstance

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<p>{{ num }}</p>
</template>
<script>
import {ref, getCurrentInstance} from 'vue'
export default {
setup() {
const num = ref(3)
const instance = getCurrentInstance()
console.log(instance)

return {num}
}
}
</script>

我们来看一下其打印结果

image

因为 instance 包含的内容太多,所以没截完整,但是主要的内容都在图上了,我们重点来看一下 ctx 和 proxy,因为这两个才是我们想要的 this 的内容

image

可以看到 ctx 和 proxy 的内容十分类似,只是后者相对于前者外部包装了一层 proxy,由此可说明 proxy 是响应式的

(14)useStore

在 Vue2 中使用 Vuex,我们都是通过 this.$store 来获取到 Vuex 实例,但上一部分说了原本 Vue2 中的 this 的获取方式不一样了,并且我们在 Vue3 的 getCurrentInstance().ctx 中也没有发现 $store 这个属性,那么如何获取到 Vuex 实例呢?这就要通过 vuex 中的一个方法了,即 useStore

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// store 文件夹下的 index.js
import Vuex from 'vuex'

const store = Vuex.createStore({
state: {
name: '前端印象',
age: 22
},
mutations: {
……
},
……
})

// example.vue
<script>
// 从 vuex 中导入 useStore 方法
import {useStore} from 'vuex'
export default {
setup() {
// 获取 vuex 实例
const store = useStore()

console.log(store)
}
}
</script>

我们来看一下打印结果

image

然后接下来就可以像之前一样正常使用 vuex 了

(15)获取标签元素

最后再补充一个 ref 另外的作用,那就是可以获取到标签元素或组件

在 Vue2 中,我们获取元素都是通过给元素一个 ref 属性,然后通过 this.$refs.xx 来访问的,但这在 Vue3 中已经不再适用了

接下来看看 Vue3 中是如何获取元素的吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
<div>
<div ref="el">div元素</div>
</div>
</template>

<script>
import { ref, onMounted } from 'vue'
export default {
setup() {
// 创建一个DOM引用,名称必须与元素的ref属性名相同
const el = ref(null)

// 在挂载后才能通过 el 获取到目标元素
onMounted(() => {
el.value.innerHTML = '内容被修改'
})

// 把创建的引用 return 出去
return {el}
}
}
</script>

获取元素的操作一共分为以下几个步骤:

  • 先给目标元素的 ref 属性设置一个值,假设为 el
  • 然后在 setup 函数中调用 ref 函数,值为 null,并赋值给变量 el,这里要注意,该变量名必须与我们给元素设置的 ref 属性名相同
  • 把对元素的引用变量 el 返回(return)出去

补充:设置的元素引用变量只有在组件挂载后才能访问到,因此在挂载前对元素进行操作都是无效的

Vue 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。另一方面,Vue 也完全能够为复杂的单页应用提供驱动。

一、Vue 基础使用

vue 的使用

下载 Vue:npm i vue

  1. 首先下载 vue,引入 vue.js
  2. js 中创建一个 Vue 对象实例
  3. 通过 el 指定 vue 管理页面的边界
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<!-- 引入vue.js文件 -->
<script src="lib/vue.js"></script>
<div id="app">
<input v-model="message" type="text"><br>
<h1>{{message}}</h1>
</div>

<!-- 下载Vue:npm i vue -->
<script>
var vm = new Vue({
/**
* el是element的简写,用来指定vue管理页面的边界,
* 也就是说只有包裹在 #app内部的元素,才会收到Vue的管理!!!
*/
el: '#app',

/**
* 页面中用的到数据都放到data对象中
*/
data: {
message: '邓紫棋喜欢你'
}

// 写Vue可能会遇到的错误:
// 1 注意:Vue 是以大写字母开头的,它是一个构造函数!!!
// 2 注意:在 Vue 中,HTML属性值无法使用 `{{}}`!!!
// 3 开发期间一定要使用未压缩版的Vue(开发版)
})

/**
* vm是Vue的对象实例,可以通过vm.$data.属性名获取data中的属性值,可以省略$data
*/
console.log(vm.message)
console.log(vm.$data.message)
console.log(vm.message === vm.$data.message)

Mustache 表达式使用

双花括号{{}} 就是 mustache 语法,用于展示 data 中的内容,mustache 中可以出现任意的 JS 表达式;

  • 表达式{{}}只能从数据对象 data 中获取数据;
  • mustache 中不能出现语句,比如:if () {} / for(var i =0 …) {} / var num = 1;
  • Mustache 语法不能作用在 HTML 元素的属性上;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<div id="app">
<h1>{{ msg }}</h1>
<p>{{ 1 + 2 }}</p>
<p>{{ ['a', 'c', 'b'] }}</p>
<p>{{ ['a', 'c', 'b'].join('-') }}</p>
<p>{{ msg + ' -- 拼接内容' }}</p>
<p>{{ age > 18 ? '成年了' : '未成年' }}</p>
<p>{{ Math.random() }}</p>
</div>
<script>
var vm = new Vue({
el:'#app',
data:{
msg:'邓紫棋金鱼嘴',
age:19
}
})
</script>

指令 v-使用

指令 (Directives) 是带有 v- 前缀的特殊属性,当表达式的值改变时,将其产生的连带影响,响应式地作用于 DOM。

1.v-text

用来设置当前元素的文本内容,相当于 DOM 对象的 innerText 或 textContent

1
<div v-text='msg'></div>

2.v-html

更新 DOM 对象的 innerHTML

1
<div v-html='htmlMsg'></div>

3.v-bind

通过 v-bind 为 HTML 元素绑定属性,使用 data 中提供的数据;
因为 v-bind:title 这种使用方式很繁琐,所以,vue 提供了一个简化语法 :title

1
2
<img v-bind:title='msg' v-bind:src='imgPath' v-bind:name='name'>
<img :title='msg' :src='imgPath' :name='name'>

4.v-on

绑定事件,支持 js 所有的事件类型, v-on 绑定的事件方法都要写在 Vue 实例中的 methods 对象中;
v-on:省略写 @

1
2
<button v-on:click='getData'>点我</button><input v-on:onfocus='getFocus'>
<button @click='getData'>点我</button><input @onfocus='getFocus'>

5.v-model

在表单元素上创建双向数据绑定;
只能用在表单元素中,注意:不同的表单元素,v-model 的表现可能会有所不同。
比如:v-model 操作文本框的 value 属性,而复选框 v-model 就是操作其选中状态;

1
2
3
4
<!-- 绑定的是文本框输入的内容 -->
<input type="text" v-model='msg'>
<!-- 绑定的是复选框是否选中 -->
<input type="checkbox" v-model='isCheck'>

6.v-for

基于源数据多次渲染元素或模板块,不仅可以渲染集合 List 也可以遍历对象 Obj;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!-- v-for 遍历list集合-->
<ul>
<!--1.item是每一项对象
使用 v-for 的时候提供 key 属性,以获得性能提升。
-->
<li v-for='item in list' :key='item.key'>
姓名:{{item.name}} -- 年龄:{{item.age}}
</li>

<!--2.item 为当前项,index 为索引 -->
<li v-for='(item,index) in list'>
姓名:{{item.name}} -- 年龄:{{item.age}} -- 下标:{{index}}
</li>
</ul>

<!-- v-for Obj对象 value,key,index顺序不能变 -->
<ul>
<li v-for='(value,key,index) in csObj'>
key={{key}} -- value={{value}} -- index={{index}}
</li>
</ul>

7.v-bind:class 和 v-bind:style

表达式的类型:字符串、数组、对象(重点)

1
2
3
4
5
<!-- 可以是对象,key是类名 value是布尔值,如果是true就添加这个类,否则就不添加-->
<h2 :class='{pink:true,green:true}'>中国惊奇先生</h2>
<!-- 可以是数组,-->
<h2 :class='['pink','fz','green']'>斗罗大陆</h2>
<h2 :style="{ color: activeColor, 'font-size': fontSize + 'px' }">不良人</h2>

8.v-if

根据表达式布尔值的真假条件是否加载这段代码, true:DOM 中会加载这段代码,false:DOM 中不会加载这段代码;

1
<h3 v-if='isIF'>我是v-if,是否会加载我</h3>

9.v-show

根据表达式之真假值,切换元素的 display CSS 属性,无论 true 还是 false DOM 中都会加载这段代码;

1
2
<h3 v-show='isShow'>我是v-show,是否会显示出来
</h3>

10.v-pre

跳过这个元素和它的子元素的编译过程。可以用来显示原始 Mustache 标签。跳过大量没有指令的节点会加快编译。

1
2
<!--测试:页面中的msg不会显示data中的内容,因为跳过了表达式编译-->
<div v-pre>v-pre跳过编译过程 {{msg}}</div>

11.v-once

只渲染元素和组件一次。随后的重新渲染,元素/组件及其所有的子节点将被视为静态内容并跳过。

1
2
<!--测试:在控制台通过vm对象修改msg,页面显示中的msg内容不会有变化-->
<div v-once>v-once跳过编译过程 {{msg}}</div>

12.v-cloak

页面中使用 {{}} 的时候,经历了由 {{}} -> 具体内容,这么一个过程,所以页面会造成“闪烁”
解决:通过添加 v-cloak 指令,配合 [v-cloak] { display: none; } 避免了页面闪烁

1
<div v-cloak>{{msg}}</div>

动态添加数据到 data、异步更新 DOM

1.动态添加数据到 data

只有 data 中的数据才是响应式的,动态添加进来的数据默认为非响应式
可以通过以下方式实现动态添加数据的响应式:

  • 1 Vue.set(object, key, value) - 适用于添加单个属性
  • 2 Object.assign() - 适用于添加多个属性

2.异步更新 DOM

当绑定的数据发生变动时,Vue 异步执行 DOM 更新,监视所有数据改变,一次性更新 DOM;
解决方法:

  • Vue.nextTick
  • this.$nextTick
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
 <div id="app">
<!-- 点击按钮之前data中还没有age属性 -->
<button @click='addAge'>给data添加age</button>
<div>名字:{{stu.name}}</div>
<!--
这里使用的表达式中的属性必须是响应式的
只有data中的数据才是响应式的,动态添加进来的数据默认为非响应式
-->
<div>年龄:{{stu.age}}</div>
<div>性别:{{stu.sex}}</div>
</div>

<script>
var vm = new Vue({
el: '#app',
data: {
msg: 'hello vue',
stu: {
name: 'jack',
}
},
methods: {
addAge: function () {
//可以通过以下方式实现动态添加数据的响应式
//1.添加单个属性
// 第一个参数:表示要给哪个对象添加响应式属性 $data可以省略
// 第二个参数:表示要添加的属性名称
// 第三个参数:表示属性的值
Vue.set(this.stu, "age", 18)
//2.添加多个属性
//第一个参数:是一个空对象
//第二个参数:添加到哪个对象
//第三个参数:添加属性的对象
this.stu = Object.assign({},this.stu,{"name":"邓紫棋","age":18,"sex":"man"})

//此时打印的内容为 名字:jack
//
//为什么呢? Vue 异步执行 DOM 更新,监视所有数据改变,一次性更新DOM
console.log(this.$el.children[1].innerText)

//解决方法 Vue.nextTick 和 this.$nextTick 是相同的
//在DOM更新后,回调执行某个操作(DOM操作)
this.$nextTick(function(){
console.log(this.$el.children[1].innerText)
})
}
}
})
</script>

filter 过滤器

  • 作用:文本数据格式化 , 也就是: 将数据按照我们指定的一种格式输出
  • 过滤器可以用在两个地方:{{}}表达式 和 v-bind 指令中
  • 两种过滤器:1 全局过滤器 2 局部过滤器

1.全局过滤器

  • 说明:通过全局方式创建的过滤器,在任何一个 vue 实例中都可以使用
  • 注意:使用全局过滤器的时候,需要先创建全局过滤器,再创建 Vue 实例
1
2
3
4
5
6
7
8
9
10
11
<div>{{ dateStr | date }}</div>
<div>{{ dateStr | date('YYYY-MM-DD hh:mm:ss') }}</div>
<script>
Vue.filter('date', function(value, format) {
// value 要过滤的字符串内容,比如:dateStr
// format 过滤器的参数,比如:'YYYY-MM-DD hh:mm:ss'
})

var vm = new Vue({
})
</script>

2.局部过滤器

  • 说明:局部过滤器是在某一个 vue 实例的内容创建的,只在当前实例中起作用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<div>{{msg | fi("九")}}</div>
var vm = new Vue({
el:'#app',
data:{
msg:'八百个标兵奔北坡,八百个标兵奔北坡'
},
//2. 局部过滤器 只有在当前Vue实例中才起作用
// 通过 filters 配置项, 来创建过滤器
filters:{
// content是内容,format是过滤的规则可以多个参数
fi:function(content,format){
return content.replace(/八/g,format);
}
}
})

watch 监听配置项

  • 概述:watch 是一个对象,键是需要观察的表达式,值是对应回调函数
  • 作用:当表达式的值发生变化后,会调用对应的回调函数完成响应的监视操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
 <div id="app">
<input type="text" v-model='userName'>
<p v-show='isError'>请输入4-8位字符</p>
<input type="text" v-model='stu.age'>
</div>
<script>
var vm = new Vue({
el: '#app',
data: {
userName: '',
isError: false,
stu: {
age: 10,
}
},
// 通过 watch 配置项,来监视数据变化
// 只能监视 data 中的数据,要监视的数据,作为watch的属性
watch: {
// 监视userName值的变化,方法名要用要监视的值的名字
userName:function(curVal, oldVal){
console.log('当前值为:', curVal, '上一次值为:', oldVal);
if(curVal.length>=4 && curVal.length<=8){
this.isError = false;
}else{
this.isError = true;
}
},
// 监听对象,加上deep:true
// 注意:如果监视对象的变化,那么,curVal 和 oldVal 是相同的,指向同一个对象
stu:{
handler:function(curVal, oldVal){
console.log('当前值为:', curVal, '上一次值为:', oldVal);
},
deep: true
},
// 一般都是监听对象中的属性
// 只需要监视某个属性的变化,而不是整个对象中所有的属性的变化
'stu.age':function(curVal, oldVal){
console.log('当前值为:', curVal, '上一次值为:', oldVal);
}
}
})
</script>

computed 计算属性配置项

  • 说明:计算属性是基于它们的依赖进行缓存的,只有在它的依赖发生改变时才会重新求值
  • 注意:Mustache 语法({{}})中不要放入太多的逻辑,否则会让模板过重、难以理解和维护
  • 注意:computed 中的属性不能与 data 中的属性同名,否则会报错
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 <div id="app">
<input type="text" v-model='num1'>+
<input type="text" v-model='num2'>=
<input type="text" v-model='result'>
</div>
<script>
var vm = new Vue({
el: '#app',
data: {
num1: 0,
num2: 0,
// result: 0 计算属性名不能和data中的属性相同
},
// 计算属性,通过 computed 配置项来指定
// 注意:计算属性不能与data中的属性相同!!!否则会报错
// 特点:计算属性依赖的属性(比如:num1 和 num2)发生改变,那么计算属性就会被重新计算
// 优势:内部使用缓存机制,如果页面中多个地方都用到了计算属性,那么计算属性只会被重新计算一次!!!
computed: {
result:function(){
return (this.num1-0)+(this.num2-0);
}
}
})
</script>

事件修饰符

  • .stop 阻止向上冒泡 不会调用父的事件
  • .prevent 阻止默认行为,调用 event.preventDefault()
  • .capture 捕获冒泡
  • .self 只当事件在该元素本身触发时,才会触发事件
  • .once 事件只触发一次
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<!-- .stop 阻止向上冒泡 不会调用父的事件 -->
<div @click='cathFather'>我是父事件修饰符
<!-- .stop 阻止冒泡,调用 event.stopPropagation() -->
<div @click.stop='catchSon'>我是子事件修饰符</div>
</div>

<!-- .prevent 阻止默认行为,调用 event.preventDefault() -->
<a href="http://www.baidu" @click.prevent='onPrevent'>我是prevent事件</a>

<!-- .capture捕获冒泡,
即有冒泡发生时,有该修饰符的dom元素会先执行,如果有多个,从外到内依次执行,然后再按自然顺序执行触发的事件。
-->
<!-- 如果不给爷爷添加capture 点击儿子触发的顺序是 儿子、爸爸、爷爷 -->
<!-- 给爷爷添加了capture事件后 点击儿子触发的顺序是 爷爷、儿子、爸爸 -->
<div @click.capture='grandpa'>我是爷爷
<div @click='father'>我是爸爸
<div @click='son'>
我是儿子
</div>
</div>
</div>

<!-- .self 只当事件在该元素本身触发时,才会触发事件 -->
<div @click.self='onSelfFather'>self事件爸爸
<div @click='onSelfSon'>self事件儿子</div>
</div>

<!-- .once 事件只触发一次 -->
<div @click.once='onOnce'>再点我一次试试</div>

键值修饰符

  • 说明:在监听键盘事件时,Vue 允许为 v-on 在监听键盘事件时添加关键修饰符
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<div id="app">
<!-- 键值修饰符 包括键盘、鼠标 -->
<!-- 13是Enter键的code值 -->
<input type="text" v-model='msg' @keyup.13='submit'>
<input type="text" v-model='msg2' @keyup.enter='submit2'><br>
<!-- 使用自定义键值修饰符 -->
<input type="text" v-model='msg' @keyup.f2='submit'>
</div>

<script>
// 自定义键值修饰符 有时候写code值是数字的时候并没有语义,所有我们给它定义一下
Vue.config.keyCodes.f2 = 113;
var vm = new Vue({
el:'#app',
data:{
msg:'',
msg2:''
},
methods:{
submit:function(){
console.log('提交数据='+this.msg)
},
submit2:function(){
console.log('提交数据='+this.msg2)
},
}
})
</script>

vue 声明周期钩子函数

1. beforeCreate()

  • 说明:在实例初始化之后,数据观测 (data observer) 和 event/watcher 事件配置之前被调用
  • 注意:此时,无法获取 data 中的数据、methods 中的方法
  • 使用场景:可以在这个钩子函数中开启页面加载的 loading 效果

2. created()

  • 注意:这是一个常用的生命周期,可以调用 methods 中的方法、改变 data 中的数据
  • 使用场景:发送请求获取数据

3. beforeMounted()

  • 说明:组件将要挂载到页面中,也就是说:组件的内容还没有被挂载到页面中
  • 注意:此时,获取不到页面中 DOM 元素

4. mounted()

  • 说明:组件已经被挂载到页面中,此时,可以进行 DOM 操作了

5. beforeUpdate()

  • 说明:数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。你可以在这个钩子中进一步地更改状态,这不会触发附加的重渲染过程。

6. updated()

  • 说明:组件 DOM 已经更新完成,所以你现在可以执行依赖于 DOM 的操作。

7. beforeDestroy()

  • 说明:实例销毁之前调用。在这一步,实例仍然完全可用。
  • 使用:实例销毁之前,执行清理任务,比如:清除定时器等

8. destroyed()

  • 说明:Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。

自定义指令

Vue 这种 MVVM 模式的框架不推荐开发人员直接手动操作 DOM 有些情况, 还是需要操作 DOM 的, 如果需要操作 DOM, 就通过 Vue 中的自定义指令来操作!!!

通过 Vue.directive()方法自定义指令:

  • 第一个参数: 表示自定义指令的名称;
  • 第二个参数 1.表示自定义指令运行的时候, 需要执行的逻辑操作;
1
2
3
Vue.directive('ff1', function (el) {
console.log(el)
})
  • 第二个参数 2.还可以是一个对象,对象中是指令的钩子函数;
1
2
3
4
5
6
7
8
9
10
 Vue.directive('ff2', {
// bind 和 inserted 这两个钩子函数, 都是进入页面就立即执行的
// 区别:inserted 能获取到指令所在元素的父元素,bind 获取不到父元素
bind(el) {
console.log('bind', el.parentNode)
},
inserted(el) {
console.log('inserted', el.parentNode)
}
)}

指令函数的入参:

1
2
3
4
5
6
7
8
9
10
11
<!-- 标识放到自定义指令的后面 .表示名 -->
<!-- 注意:如果 v-color="red" 那么,red指的值:data中的red属性 -->
<div v-color.back=" 'blue' ">{{ msg }}</div>
<div v-color.col=" 'red' ">{{ msg }}</div>
Vue.directive('color', function (el, binding) {
if (binding.modifiers.col) {
el.style.color = binding.value
} else {
el.style.backgroundColor = binding.value
}
})

小案例

案例请到https://github.com/pengjunshan/WebPJS中查看
image

二、Vue 组件

组件是可复用的 Vue 实例,组件分为全局组件和局部组件。因为组件是可复用的 Vue 实例,所以它们与 new Vue 接收相同的选项,例如 data、computed、watch、methods 以及生命周期钩子等,el 是根实例特有的属性,组件中没有。

全局组件

全局组件在所有的 vue 实例中都可以使用,注意:先注册组件,再初始化根实例。

Vue.component(‘name’,{配置项})

  • 第一个参数是组件名
  • 第二个参数是组件的配置项,与 Vue 根实例配置项差不多
  • 组件中的 data 必须是个函数数据用 return 返回,Vue 根实例中的 data 是个对象
  • 组件中 template 模板有两种方式,“字符串” “html 模板”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<body>
<div id="app">
<hello></hello>
</div>

<template id="temp">
<div>
<p>我是组件:{{msg}}</p>
<button @click='fn'> 点击消灭新冠 </button>
</div>
</template>
<script>

/**
* 第一个参数是组件名
* 第二个参数是配置项,与Vue实例的配置几乎一样
*/
Vue.component('hello', {
//template是组件的模板 也就是要展示的内容
//组件中template模板有两种方式,“字符串” “html模板”
//方式一:字符串
// template: `
// <div>
// <p>我是组件:{{msg}}</p>
// <button @click='fn'> 点击消灭新冠 </button>
// </div>
// `,
//方式二:html模板
template: '#temp',

//组件中的data必须是个函数数据用return返回,Vue根实例中的data是个对象
data() {
return {
msg: '武汉加油 中国加油'
}
},
methods: {
fn() {
this.msg = '新冠被消灭了,中国威武'
}
}
})
var vm = new Vue({
el: '#app',
data: {}
})
</script>
</body>

局部组件

局部组件,是在某一个具体的 vue 实例中定义的,只能在这个 vue 实例中使用;在 Vue 实例中使用 components 对象创建组件,可以创建多个组件;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
<body>
<div id="app">
<!-- hello组件 -->
<hello></hello>
<!-- love组件 -->
<love></love>
</div>

<template id="temp">
<div>
<p>我是组件222:{{msg}}</p>
<button @click='fn'> 点击消灭新冠 </button>
</div>
</template>

<script>

//1.局部组件,是在某一个具体的vue实例中定义的,只能在这个vue实例中使用
//2.在Vue实例中使用components对象创建组件,可以创建多个组件
//3.组件中template模板有两种方式,“字符串” “html模板”

var vm = new Vue({
el: '#app',
data: {
},
components: {
//hello 组件名
'hello': {
//方式一 字符串
template: `
<div>
<p>我是组件111:{{msg}}</p>
<button @click='fn'> 点击消灭新冠 </button>
</div>
`,
data() {
return {
msg: '武汉加油 中国加油'
}
},
methods: {
fn() {
this.msg = '新冠被消灭了,中国威武'
}
}
},
'love': {
//方式二 引用html中的代码模板
template: '#temp',
data() {
return {
msg: '我们爱中国 爱武汉'
}
},
methods: {
fn() {
this.msg = '我们爱中华民族'
}
}
}

}
})
</script>
</body>

父组件传递子组件数据

  • 方式:通过 props 属性来传递数据
  • 注意:属性的值必须在组件中通过 props 属性显示指定,否则,不会生效
  • 说明:传递过来的 props 属性的用法与 data 属性的用法相同
  • 如果传递的数据是 data 中的属性时,必须使用 v-bind 绑定属性才可以传递过去
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<body>
<div id="app">
<hello mm='中华民族万岁'></hello>
<!-- 如果传递的数据是data中的属性时,必须使用v-bind绑定属性才可以传递过去 -->
<!-- <hello zz='msg'></hello> -->
<hello v-bind:zz='msg'></hello>
</div>

<script>

// 父组件 传递数据给 子组件:(父组件:Vue的实例对象,子组件:hello组件)
// 原理:通过 props 属性来传递
// 注意:使用父组件传递的属性方式和使用data中的属性方式一样
Vue.component('hello', {
template: `
<div>
<p>我是组件:{{msg}}</p>
<p v-if='mm'>我接收到父组件的内容:{{mm}}</P>
<p v-if='zz'>我接收到父组件的内容:{{zz}}</P>
<button @click='fn'> 点击消灭新冠 </button>
</div>
`,
//指定props中的值,来接收父组件传递过来的值
props:['mm','zz'],
data() {
return {
msg: '武汉加油 中国加油'
}
},
methods: {
fn() {
this.msg=this.msg+'-----'+(this.mm===undefined? this.zz:this.mm);
}
}
})
var vm = new Vue({
el: '#app',
data: {
msg:'浙江杭州'
}
})
</script>
</body>

子组件传递父组件数据

  • 方式:父组件给子组件传递一个函数,由子组件调用这个函数
  • 说明:借助 vue 中的自定义事件(v-on:cunstomFn=”fn”)
  • $emit():触发事件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<body>
<div id="app">
<p>{{msg}}</p>
<hello @pfn='parentFn'></hello>
</div>
<template id="temp">
<div>
<p>我是组件:{{msg}}</p>
<button @click='sonFn'> 点击消灭新冠 </button>
</div>
</template>
<script>
//组件传递 子》父
//1.由父组件定义一个方法,通过@pfn传给子组件
//2.子组件通过$emit方法把数据传递给父组件定义的方法

Vue.component('hello', {
template: '#temp',
data() {
return {
msg: '武汉加油 中国加油'
}
},
methods: {
sonFn() {
//通过$emit方法传递数据给父组件的方法,参数可以为多个
this.$emit('pfn', '新冠被消灭了,中国威武', '测试')
}
}
})

var vm = new Vue({
el: '#app',
data: {
msg: '标题'
},
methods: {
parentFn(data, data2) {
this.msg = data;
console.log(data2)
}
},
})
</script>
</body>

非父子组件传递数据

  • 可以使用一个空的 Vue 实例作为事件总线 bus
  • A 组件传递 B 组件数据 1.B 先通过 bus.$on绑定事件,2.A通过bus.$emit 方法调用 B 绑定的事件方法
  • $on和$emit 都是 bus 调用的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<body>
<div id="app">
<aaa></aaa>
<bbb></bbb>
</div>
<script>
//可以使用一个空的 Vue 实例作为事件总线
var bus = new Vue()
//A组件传递B组件数据 1.B先通过bus.$on绑定事件,2.A通过bus.$emit方法调用B绑定的事件方法
//$on和$emit都是bus调用的
var vm = new Vue({
el: '#app',
data: {},
components: {
aaa: {
template: `
<div>
<h3>我是组件AA</h3>
<button @click='fn'> 点我传给B数据 </button>
</div>
`,
data() {
return {
msg: 'A组件'
}
},
methods: {
fn() {
bus.$emit('bfn', '组件A说:你好组件B')
}
}

},
bbb: {
template: `
<div>
<h3>我是组件BB</h3>
<p>{{msg}}</p>
</div>
`,
data() {
return {
msg: '我在等待数据...'
}
},
created() {
//绑定事件 接收数据,当进入页面时走到这个钩子函数后就自动绑定事件了
bus.$on('bfn', data => {
this.msg = data
})
}
}
}
})
</script>
</body>

组件中插槽使用

有时在使用组件的时候希望往组件中加入其它内容,在组件中通过插槽来接收内容。插槽内可以包含任何模板代码,包括 HTML,甚至其它的组件;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<body>
<div id="app">
<hello>
<p>赶走新冠</p>
</hello>
</div>

<template id="temp">
<div>
<p>{{msg}}</p>
<slot></slot>
<slot></slot>
</div>
</template>

<script>

//当组件渲染的时候,<slot></slot> 将会被替换为"赶走新冠"。插槽内可以包含任何模板代码
//可以有多个插槽
Vue.component('hello',{
template:'#temp',
data(){
return{
msg:'武汉加油 中国加油'
}
}
})
var vm = new Vue({
el: '#app',
data: {
msg:'赶走新冠'
}
})
</script>
</body>

vue 中 ref 使用

使用 ref 注册后 可以使用 this.$refs 获取当前 DOM 对象,必须在 mounted()钩子函数之后才可以获取 DOM 对象;元素和组件都可以使用 ref 注册;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<body>
<div id="app">
<!-- 使用ref注册后 可以使用this.$refs.pp获取当前DOM对象 -->
<p ref='pp'>哈喽,大家好</p>

<!-- 组件也可以 -->
<hello ref="ho"></hello>
</div>

<script>
var vm = new Vue({
el: '#app',
components: {
hello: {
template: `<h1>大家好</h1>`,
data() {
return {
msg: 'hello message'
}
},
methods: {
fn() {
console.log("触发了事件11111")
}
}
}
},
mounted() {
console.log(this.$refs.pp)
this.$refs.pp.style.color = 'red';

console.log(this.$refs.ho.msg)
this.$refs.ho.fn()
}
})
</script>
</body>

三、vue-router 路由

路由基本使用

  • 1.在当前文件夹下执行 npm init、npm init -y 初始化 package.json
  • 2.然后执行 npm i -s vue、npm i -s vue-router 安装 vue 和 vue-router
  • 3.在 node_modules 下找到 vue 和 vue-router 中的 js 并引入到项目中
  • 4.创建组件
  • 5.创建路由对象,并配置路由
  • 6.在 Vue 实例中关联 router
  • 7.路由入口
  • 8.路由出口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<body>
<div id="app">
<!-- 6.路由入口 -->
<router-link to='/home'>首页</router-link>
<router-link to='/me'>我的</router-link>
<!-- 7.路由出口 -->
<router-view></router-view>
</div>
<script src="./node_modules/vue/dist/vue.js"></script>
<script src="./node_modules/vue-router/dist/vue-router.js"></script>
<script>
//首先先安装vue和vue-router
//1.在当前文件夹下执行npm init、npm init -y初始化package.json
//2.然后执行npm i -s vue、npm i -s vue-router安装vue和vue-router
//3.在node_modules下找到vue和vue-router中的js并引入到项目中
//4.先创建两个组件
const Home = Vue.component('home', {
template: `<h1>我是Home组件</h1>`
})
const Me = Vue.component('me', {
template: `<h1>我是Me组件</h1>`
})
//5.创建路由对象
const router = new VueRouter({
routes: [
{ path: '/home', component: Home },
{ path: '/me', component: Me }
]
})
var vm = new Vue({
el: '#app',
data: {},
//将vue和router
router: router
})
</script>
</body>

重定向、高亮

当第一次打开页面时,想要默认打开一个路由,就可以使用路由中的重定向;按钮的高亮样式不是我们喜欢的,可以使用 linkActiveClass 来自定义高亮元素的类名,然后再设置 css 样式就可以了;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 //5.创建路由对象
const router = new VueRouter({
routes: [
//如果当前路径是'/'就redirect重定向 默认home组件
{ path: '/', redirect: '/home' },
{ path: '/home', component: Home },
{ path: '/me', component: Me }
],
// 修改默认高亮的a标签的类名
// 如果是配合第三方组件库来实现菜单高亮,此时,只需要将类名设置为 第三方组件的类名即可
linkActiveClass: 'now'
})
<style>
/* .router-link-exact-active, */
/* .router-link-active { */
.now {
color: hotpink;
font-size: 30px;
text-decoration: none;
}
</style>

路由传参方式

有时候多个路由都打开同一个组件,可以通过给组件传不同的参数展示不同的内容就可以了;通过路由打开不同组件传参也是一样的;
导航分为两种

  • 1.声明式导航(router-link)
  • 2.编程式导航($router.push)
  • to 字符串:只能传递字符串
  • :to 对象:可以传递对象,可以通过 name、path 方式

编程式导航($router.push)

  • this.router.push(name,params);通过 name 跳转的状态栏里看不到参数类似 post;组件通过 route.params 获取参数;
  • this.router.push(path,query);通过 path 跳转的状态栏里可以看到参数类似 get;组件通过 route.query 获取参数;

监听路由变化

在组件中通过 watch 对象中监听路由的变化$route(to, from) {},在这里可进行监听数据变化 进行网络请求等等;

详细使用方式请看下面代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
<body>
<div id="app">
<!-- 通过to字符串 -->
<router-link to="/home/1001">1001号赛车</router-link>
<router-link to="/home/1002">1002号赛车</router-link>
<!-- 通过$router.push{} -->
<a @click='fn'>1003号赛车</a>
<a @click='fn1'>1004号赛车</a>
<!-- 通过:to对象 -->
<router-link :to="{ name:'Home',params:{id:1005,title:'pjs'}}">1005</router-link>
<router-link :to="{ path:'/home',query:{id:1006,title:'pjs'}}">1006</router-link>

<router-view></router-view>
</div>

<script>
//导航分为两种:1.声明式导航(router-link) 2.编程式导航($router.push)
//1.通过to字符串
//2.$router.push{}
//3.通过:to对象

//通过name跳转的状态栏里看不到参数类似post,通过path跳转的状态栏里可以看到参数类似get
//通过$route.params或$route.query来获取参数
const Home = Vue.component('home', {
template: `
<div>
<h1 v-if='$route.params.id'>欢迎来到主页面{{ $route.params.id}}</h1>
<h1 v-if='$route.query.id'>欢迎来到主页面{{ $route.query.id}}</h1>
</div>
`,
//监听路由变化,获取参数进行操作
watch: {
//只要路由发生的变化就会执行这个方法,to 跳转的目的地, from 从哪里来
$route(to, from) {
//在这里可进行监听数据变化 进行网络请求
console.log(to)
console.log(from)
console.log(to.params.id)
console.log(to.query.id)
}
}
})

const router = new VueRouter({
routes: [
{ path: '/home/:id', component: Home },
{ path: '/home', name: 'Home', component: Home }
]
})

var vm = new Vue({
el: '#app',
router,
methods: {
fn() {
this.$router.push({
name: 'Home',//找到routes里匹配到name为Home
params: {
id: 1003,
title: 'pjs'
}
})
},
fn1() {
this.$router.push({
path: '/home',
query: {
id: 1004,
title: 'pjs'
}
})
}
}
})
</script>
</body>

路由嵌套-子路由

  • 路由是可以嵌套的,即:路由中又包含子路由
  • 规则:父组件中包含 router-view,在路由规则中使用 children 配置
  • 使用 children 里配置子路由,子路由的 path 里不需要加/符号了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<body>
<div id="app">
<router-link to='/home'>首页</router-link>
<router-link to='/user'>我的</router-link>
<router-view></router-view>
</div>

<script>

//路由是可以嵌套的,即:路由中又包含子路由
//规则:父组件中包含 router-view,在路由规则中使用 children 配置
const Home = Vue.component('home', {
template: `
<div>
<router-link to='/home/cartA'>买车</router-link>
<router-link to='/home/cartB'>卖车</router-link>
<router-view></router-view>
</div>
`
})
const CartA = {
template: `<h3>买什么样的车?</h3>`
}
const CartB = {
template: `<h3>卖什么样的车?</h3>`
}
const User = Vue.component('user', {
template: `
<div>
这里是个人信息
</div>
`
})
const router = new VueRouter({
routes: [
{
path: '/home', component: Home,
//使用children里配置子路由,子路由的path里不需要加/符号了
children: [
{
path: 'cartA',
component: CartA
},
{
path:'cartB',
component:CartB
}
]
},
{ path: '/user', component: User }
]
})
var vm = new Vue({
el: '#app',
router
})
</script>
</body>

四、vuex

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。意思就是所有组件共同操作一份数据,都可以对它进行增删改查。

属性 作用
State 共享数据都存在这里
Mutation 更改 State 中数据的唯一方法,同步操作
Action 异步操作 Mutation 来更改 State 中的数据
Getter 基于 state 的派生状态,可理解为组件中的计算属性

State

  • 1.安装 vuex npm i -s vuex
  • 2.创建 store 对象
  • 3.把 vue 和 store 进行关联
  • 4.使用$store.state 中的数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<body>
<div id="app">
<!-- 4.使用$store.state中的数据 -->
<h1>{{this.$store.state.name}}</h1>
</div>
<script>
//1.安装vuex npm i -s vuex
//2.创建store对象
let store = new Vuex.Store({
/**
* state中可以存储任何类型的值
*/
state: {
name: '邓紫棋'
}
})
let vm = new Vue({
el: '#app',
data: {},
//3.把vue和store进行关联
store,
mounted() {
//可以获取store对象
console.log(this.$store)
}
})
</script>
</body>

image

Mutation

mutation 是更改 store 中状态的唯一方法,vuex 中规定只能通过提交 mutation 的方式去更改 store 中的状态,store.commit()方法更改数据。

  • 无参数
1
2
3
4
5
6
//调用方 只传一个方法名
this.$store.commit('changeName')
//接收方 默认第一个参数是state,无参接收
changeName(state) {
state.name = '张韶涵'
}
  • 载荷提交,只能提交一个参数
1
2
3
4
5
6
//调用方,第一个参数:方法名,第二个参数:参数
this.$store.commit('changeName',this.msg)
//接收方
changeName(state, name) {
state.name = name ? name : '张韶涵';
}
  • 载荷对象提交
1
2
3
4
5
6
7
8
9
//调用方,传递一个对象
this.$store.commit('changeName',{
name:this.msg
})
//接收方
changeName(state, payload) {
state.name = payload.name ;
state.sex = payload.sex//给state新增一个属性
}
  • 纯对象风格提交 type 值是方法名
1
2
3
4
5
6
7
8
9
10
11
//提交一个纯对象当做参数,type值为方法名
this.$store.commit({
type: 'changeName',
name: this.msg,
sex: '男'
})
//接收方
changeName(state, payload) {
state.name = payload.name ;
state.sex = payload.sex//给state新增一个属性
}
  • 案例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
<body>
<div id="app">
<!-- 组件AAA和组件BBB共同操作$store.state中的数据 -->
<!-- 比我们之前学的组件之间通讯更加方便 -->
<AAA></AAA>
<BBB></BBB>
</div>
<script>
/**
* mutation是更改store中状态的唯一方法
* vuex中规定只能通过提交mutation的方式去更改store中的状态
* store.commit()更改数据
*/
let store = new Vuex.Store({
state: {
name: '邓紫棋'
},

/**
* mutations对象中自定义方法来操作state中的数据
* 并且每个方法会接受 state 作为第一个参数
* 注意:Store对象中写mutations;Mutation 必须是同步函数
*/
mutations: {
//不接收参数
changeName(state) {
state.name = '张韶涵'
},
//只接收一个参数
changeName(state, name) {
state.name = name ? name : '张韶涵';
},
//接收参数对象
changeName(state, payload) {
state.name = payload.name ? payload.name : '张韶涵';
state.sex = payload.sex//给state新增一个属性
}
}
})

//创建两个组件
const AAA = Vue.component('aaa', {
template: `
<div>
<!-- 4.使用$store.state中的数据 -->
<h1>{{this.$store.state.name}}</h1>
<h2>{{this.$store.state.sex}}</h2>
</div>
`
})
const BBB = Vue.component('bbb', {
template: `
<div>
<input type="text" placeholder="请输入姓名" v-model='msg'>
<input type="button" value="确定" @click='change'>
</div>
`,
data() {
return {
msg: ''
}
},
methods: {
change() {
//1.无参数
// this.$store.commit('changeName')

//2.载荷提交,只能提交一个参数
// this.$store.commit('changeName',this.msg)

//3.载荷对象提交
// this.$store.commit('changeName',{
// name:this.msg
// })

//4.纯对象风格提交 type值是方法名
this.$store.commit({
type: 'changeName',
name: this.msg,
sex: '男'
})
}
}
})
let vm = new Vue({
el: '#app',
//把vue和store进行关联
store
})
</script>
</body>

image

Action

mutation 中规则上是不允许异步操作的,于是 vuex 为我们提供了 action。actions 对象中自定义方法来操作 mutations,并且每个方法会接受 context 作为第一个参数,context 对象与 store 对象具有相同的方法和属性;action 与 mutation 除了使用了异步操作和调用 mutation,其它使用并无差别 ;

  • 异步更新数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//action事件的触发同样可以使用载荷和对象两种方式,其它方式就不写了和mutations方式一样
this.$store.dispatch({
type:'changeNameAsync',//Store.actions中的方法名
name: this.msg,
sex: '男'
})
actions: {
//接收数据 延迟一秒提交数据
changeNameAsync(context, payload) {
//异步操作
setTimeout(() => {
context.commit('changeName',payload)
}, 1000)
}
}
mutations: {
//接收参数对象
changeName(state, payload) {
state.name = payload.name ;
state.sex = payload.sex//给state新增一个属性
}
}
  • 案例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
<body>
<div id="app">
<AAA></AAA>
<BBB></BBB>
</div>
<script>
/**
* action--异步更改状态
* mutation中规则上是不允许异步操作的,于是vuex为我们提供了action。
* store.dispatch() 方法触发
*/
let store = new Vuex.Store({
state: {
name: '邓紫棋'
},

/**
* mutations只能同步执行
*/
mutations: {
//接收参数对象
changeName(state, payload) {
state.name = payload.name ? payload.name : '张韶涵';
state.sex = payload.sex//给state新增一个属性
}
},
/**
* actions对象中自定义方法来操作mutations,并且每个方法会接受 context 作为第一个参数;
* context对象与store对象具有相同的方法和属性
* action 与 mutation 除了使用了异步操作和调用mutation,其它使用并无差别
*/
actions: {
//接收数据
changeNameAsync(context, payload) {
//异步操作
setTimeout(() => {
context.commit({
type: 'changeName',
name: payload.name,
sex: '男'
})
}, 1000)
}
}
})

//创建两个组件
const AAA = Vue.component('aaa', {
template: `
<div>
<!-- 4.使用$store.state中的数据 -->
<h1>{{this.$store.state.name}}</h1>
<h2>{{this.$store.state.sex}}</h2>
</div>
`
})
const BBB = Vue.component('bbb', {
template: `
<div>
<input type="text" placeholder="请输入姓名" v-model='msg'>
<input type="button" value="确定" @click='change'>
</div>
`,
data() {
return {
msg: ''
}
},
methods: {
change() {
//action事件的触发同样可以使用载荷和对象两种方式,其它方式就不写了和mutations方式一样
this.$store.dispatch({
type:'changeNameAsync',//Store.actions中的方法名
name: this.msg
})
}
}
})
let vm = new Vue({
el: '#app',
store
})
</script>
</body>

image

getter

getters 类似 Vue 实例中的计算属性,当绑定的属性发生变化后才会重新计算;每个方法都默认接收 state 参数。

  • getters 使用
1
2
3
4
5
6
7
8
9
10
11
12
//在getters下创建一个getName方法,默认接收state,此方法和计算属性用法一样,当state中的name发生改变时会重新计算这个方法return结果
getters:{
getName(state){
let myName='';
if(state.name === '彭俊山'){
myName = '你是最帅的!'
}
return state.name + myName;
}
}
//在A组件中使用getters下的getName属性,当B组件修改了state中的name后A组件中h2数据也会变化
<h2>{{this.$store.getters.getName}}</h2>
  • 案例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
<body>
<div id="app">
<AAA></AAA>
<BBB></BBB>
</div>
<script>
let store = new Vuex.Store({
state: {
name: '邓紫棋'
},
mutations: {
//接收参数对象
changeName(state, payload) {
state.name = payload.name ? payload.name : '张韶涵';
}
},
/**
* getters类似Vue实例中的计算属性,当绑定的属性发生变化后才会重新计算
* 每个方法都默认接收state参数
*/
getters:{
getName(state){
let myName='';
if(state.name === '彭俊山'){
myName = '你是最帅的!'
}
return state.name + myName;
}
}
})

//创建两个组件
const AAA = Vue.component('aaa', {
template: `
<div>
<!-- 使用$store.state中的数据 -->
<h1>{{this.$store.state.name}}</h1>
<!-- 使用$store.getters中的属性 -->
<h2>{{this.$store.getters.getName}}</h2>
</div>
`
})
const BBB = Vue.component('bbb', {
template: `
<div>
<input type="text" placeholder="请输入姓名" v-model='msg'>
<input type="button" value="确定" @click='change'>
</div>
`,
data() {
return {
msg: ''
}
},
methods: {
change() {
//纯对象风格提交 type值是方法名
this.$store.commit({
type: 'changeName',
name: this.msg
})
}
}
})
let vm = new Vue({
el: '#app',
store
})
</script>
</body>

image

vuex 刷新页面 store 数据丢失

当刷新页面后,store 中的数据都会丢失;将 store 的数据存储在 storage 里,由于 vue 多为单页面应用,且每次重新打开页面需要保持数据为空 所以这里我们不选用 localStorage,用 sessionStorage 会话机制;

1
2
3
4
5
6
7
8
9
10
11
created() {
//在页面加载时读取sessionStorage里的状态信息
if (sessionStorage.getItem("store")) {
this.$store.replaceState(Object.assign({}, this.$store.state, JSON.parse(sessionStorage.getItem("store"))))
}

//在页面刷新时将vuex里的信息保存到sessionStorage里
window.addEventListener("beforeunload", () => {
sessionStorage.setItem("store", JSON.stringify(this.$store.state))
})
}

一、Angular 概述

基于命令行的开发方式?

1
2
3
4
①hot reload
②编译工作
③集成了webpack打包工具
。。。。

1、what?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
angular是一个Google推出的js框架,是以模块为基本单位,模块又可以包含组件、指令、过滤器。。

1.1 版本问题
angular angular2.0以后所有的版本统称为angular
(当前学习ng4.0)

angular.js angular1.* 统称为angular.js
(http://www.runoob.com/angularjs/angularjs-tutorial.html)

1.2 版本之间的区别
①新版本是有组件的概念的
②老版本是$scope和controller为主
③angular引入了rxjs
④angular采用ts(typescript是es6的超集,是由微软和谷歌) ts是一种强类型检查机制的语言
⑤angular可读性、提高了代码的复用率、维护成本变低。。。

2、where

1
2
3
可以使用支持angular的Ionic框架来实现移动端的开发,直接使用angular来实现pc端的开发

实现操作比较频繁的SPA

3、why

1
2
3
4
①遵循w3c所推出的webComponent标准(组件化)
②代码具有更好的可读性和可维护性、
③引入了更多的高效率的工具 ,比如rxjs\immutable.js。。。, 让代码的编译、部署更简单
④ts --》 健壮

4、how

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
angular的开发整体框架,是有8大组成部分构成

搭建环境的方式:
方式1:
①下载quickstart-master.zip压缩包
https://github.com/angular/quickstart download
或者 直接拷贝老师提供的压缩包
②解压缩 压缩包,进入对应的目录中
执行npm install 安装项目所需要用到的依赖
③npm start 启动开发服务器

方式2:
Angular CLI是一个命令行界面工具,它可以创建项目、
添加文件以及执行一大堆开发任务,比如测试、打包和发布。
//安装基于angular的命令工具
npm install -g @angular/cli
//创建一个有着ng模板的项目
ng new my-app
//进入当前目录下的my-app
cd my-app
//启动开发服务器
ng serve --open

二、Angular 模板项目的启动流程

1
2
3
4
5
6
7
index.html

main.js (main.ts)-->启动一个模块 AppModule

app/app.module.ts ---> 启动一个组件 app/app.component.ts

Hello Angular

三、完成组件的创建和使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
1、创建组件和使用组件
①创建文件 app/test/test.component.ts
②将类装饰成一个组件类
import {Component} from '@angular/core'

@Component({
selector:'test',
template:`<h1>it is a test</h1>`
})

export class Demo01Component{
}
③使用组件
①到模块中声明
app.module.ts中,
import {TestComponent} from './test/test.component'

@NgModule({
declarations:[TestComponent]
})
②<test></test>

练习:(16:50 - 17:00)
demo02/demo02.component.ts
组件中渲染一个无序列表(5个列表)

将组件渲染AppComponent

四、Angular 中常见的指令

1
2
3
4
5
6
7
8
9
10
11
12
1、循环指令
Vue : <any v-for="tmp in list"></any>

Angular:
语法:
<any *ngFor="let tmp of list"></any>
<any *ngFor="let tmp of list;let myIndex=index"></any>

2、选择指令
Vue: <any v-if="表达式"></any>
angular:
<any *ngIf="表达式"></any>

五、常见指令

指令和组件的关系:

1
组件就是一个带有模板的指令!!!

1、多重分支判断

1
2
3
4
5
6
7
8
9
vue
v-if
v-else-if
v-else

<div [ngSwitch]="answer">
<p *ngSwitchCase="'a'"></p>
<p *ngSwitchDefault></p>
</div>

2、属性绑定

1
2
3
4
5
6
7
8
9
10
11
Vue:
<img v-bind:src="imgUrl"/>
<img :src="imgUrl"/>
<button :class="{myHightlight:true}"></button>
<h1 :style="{backgroundColor:myBG}"></h1>

Angular:
<img [src]="imgUrl"/>
<button [ngClass]="{myHightlight:true}"></button>
<h1 [ngStyle]="{backgroundColor:myBG}">
</h1>

3、事件绑定

1
2
3
4
5
6
7
8
Vue
<button v-on:click="handleClick"></button>
<button @click="handleClick"></button>
Angular
语法:
<any (eventName)="eventHandler()"></any>
举例:
<button (click)="handleClick()"></button>

4、双向数据绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Vue:
<input v-model="addr"/>
Angular:
<input [(ngModel)]="addr"/>

依赖注入:
将依赖的东西注入到指定的地方,让依赖可被使用
举例:AppModule依赖于FormsModule,
只需要在AppModule的imports数组写上FormsModule名称
就可以使用FormsModule所提供的东西。
好处:解耦,降低了耦合度

Angular中如果想要监听双向数据绑定数据的变化,提供一个事件 ngModelChange

注意事项:
①Angular中如果要想使用双向数据绑定,就必须指定模块依赖于FormsModule
②使用ngModelChange事件时,通过$event去传递用户当前所输入的信息
(ngModelChange)="handleChange($event)"

内置的指令:

1
2
3
4
5
6
7
8
9
*ngFor
*ngIf
*ngSwitchCase
*ngSwitchDefault
ngSwitch
[]
()
[(ngModel)]
{{}}

5、自定义指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
Vue中自定义指令:
Vue.directive('change',{
bind:function(el,binding){},
update:function(){},
unbind:function(){}
});
v-change
Angular中指令创建和使用

5.1 创建
import {Directive} from '@angular/core'

@Directive({
selector:'[test]'
})

export class TestDirective{
}

5.2 使用
①到模块中声明
app.module.ts
import {TestDirective} from '***'
@NgModule({
declarations:[TestDirective]
})
②作为标签的属性
<h1 test></h1>

5.3 得到调用指令的元素
①import {ElementRef} from '@angular/core'
②实例化
constructor(private el:ElementRef){}
③读取元素
this.el.nativeElement

5.4 指令调用时传参??
①<h1 test="123"></h1>
②在指令类的内部
import {Input} from '@angular/core'

@Input() test="";

this.test

补充:使用生命周期的处理函数?
①引入
import {OnDestroy} from '@angular/core'
②在定义类的时候 实现接口类
export class Test implements OnDestroy{
ngOnDestroy(){}
}

六、组件之间通信

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
Vue中组件通信的方式?
①props down
步骤1:发送
<son myName="zhangsan"></son>
步骤2:接收
Vue.component('son',{
props:['myName']
})
②events up(子-》父)
步骤1: 事件的绑定
methods:{
rcvMsg:function(msg){}
}
<son @customEvent="rcvMsg"></son>
步骤2:事件的触发(儿子)
this.$emit('customEvent',123);
③$refs $parent
④bus
Angular中组件通信?
1、props down
步骤1:发送
<son uName="zhangsan"></son>
步骤2:接收
import {Input} from '@angular/core'
@Input() uName="";
this.uName
2、events up
步骤1:事件和处理函数的绑定
定义一个方法
rcvMsg(msg){}
<son (toFatherEvent)="rcvMsg($event)">
</son>
步骤2:触发事件
子组件触发
import {Output,EventEmitter} from '@angular/core'

@Output() toFatherEvent = new EventEmitter();

this.toFatherEvent.emit('123');

我们是这样写 Angular 应用的:

1
2
3
4
用 Angular 扩展语法编写 HTML 模板,
用组件类管理这些模板,
用服务添加应用逻辑,
用模块打包发布组件与服务。

七、管道(pipe)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
管道是用来对数据进行筛选、过滤、格式化

Vue中过滤器:
<any>{{expression | filter(1,2) | filter2 }}</any>

Vue.filter('changeSex',function(arg,arg1,arg2){
return 处理后的结果
})

angular中管道:

过滤器的本质就是一个有参数有返回值的方法

语法:
<any>
{{expression | pipe1:'12':34 | pipe2}}
</any>

1、内置管道

常见内置管道:
uppercase/lowercase/date/number/slice

2、自定义管道
创建一个自定义管道:
import {Pipe,PipeTransform} from '@angular/core'

@Pipe({
name:'testNG'
})

export class TestPipe implements PipeTransform {
//value是竖杠前表达式执行的结果
//args通过调用管道时,冒号后边跟的参数
transfrom(value:any,...args:[]):any{
return ‘处理后的结果’
}

}

调用:
①声明
到模块中先引入再声明
②调用
和内置管道的用法是一样的,同样支持传参、多重过滤

八、服务 (依赖注入)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
服务 service:服务的本质是一个类,在服务类中封装的是经常用到的数据和方法。

常见的服务:日志类服务、心跳服务、网络请求服务。。。

1、服务的创建和使用
创建:
import {Injectable} from '@angular/core'

@Injectable()
export class UserService {
constructor(){}

checkUserLogin(){return true}
}

使用:
①需要给服务指定provider
在组件中或者模块中指定providers:[UserService]
②调用
import {UserService} from './***'

constructor(private myService:UserService){}

this.myService.checkUserLogin()

2、如何封装一个网络请求的服务

①创建服务的文件
②在服务中封装一个方法
sendRequest(myUrl:string){
return this.http.get(myUrl).map((response)=>
response.json()
)
}
③调用之前 首先指定providers
④到组件中,先引入,再实例化,再调用
this.myHS.sendRequest().subscribe((result)=>{
//result就是服务器端返回的结果!
})

与服务器端通信如果涉及的session,angular需要这么处理:
客户端
①发起请求 withCredentials:true
this.http.get(
myUrl,
{withCredentials:true}
)
服务器端:
①跨域header('Access-Control-Allow-Origin:http://localhost:3000');
②服务器允许接收凭证
header('Access-Control-Allow-Credentials:true');

服务创建和使用:
1、创建一个文件 test.service.ts
2、在文件中编写代码,装饰一个服务
@Injectable()
export class TestService{
showAlert(msg){
alert(msg);
}
}
3、 给模块或者组件,在providers属性对应的数组中 [TestService]

4、组件中要想使用服务中的方法
import {TestService} from '***'

constructor(private myService:TestService){}

this.myService.showAlert()

Angular 中开发模式:

1
2
3
4
5
6
7
8
我们是这样写 Angular 应用的:
用 Angular 扩展语法编写 HTML 模板,
用组件类管理这些模板,
用服务添加应用逻辑,
用模块打包发布组件与服务。

然后,我们通过引导根模块来启动该应用。
Angular 在浏览器中接管、展现应用的内容,并根据我们提供的操作指令响应用户的交互。

在 Angular 开发时,八大组成部分:

1
2
3
4
5
6
7
8
9
10
11
12
1、模块
2、组件
3、模板 自带的html标签+指令、绑定相关的ng的语法
4、元数据 告诉 Angular 如何处理一个类。
5、数据绑定
{{}} () [] [(ngModel)]
6、指令
三大类:组件、结构型、属性型
7、服务
封装一些数据和方法
8、依赖注入
就是将依赖的服务、模块注入到指定组件、模块中使用,提供了一种新的实例化的方式(解耦)

九、路由模块

路由模块:建立起 url 和页面之间的映射关系

1、实现 SPA 的基本步骤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
Vue:
实现一个SPA基本思路:
①指定一个容器
<router-view></router-view>
②创建代码片段
创建组件
var Login = Vue.component('login-component',{
template:`<h1>登录页面</h1>`
})
③配置路由词典
new Vue({
router:new VueRouter({
routes:[
{path:'/myLogin',component:Login}
]
})
})
④测试
测试路由词典中 路由地址能否按照需求 正确加载所需要用到的页面

Angular:
①指定容器
<router-outlet></router-outlet>
②创建组件 (声明)
@Component({}) export class **
③配置路由词典
//a-module-routing
import {Routes,RouterModule} from '@angular/router'
import {LoginComponent} from './demo15_spa/login.component'

const routes:Routes = [
{path:'',component:LoginComponent}
.....
]

@NgModule({
import:[RouterModule.forRoot(routes)],
exports:[RouterModule]
})

export class AppRoutingModule{}

找到根模块:
import {AppRoutingModule} from './app.router'

@NgModule({
imports:[AppRoutingModule]
})
④测试

2、在 Angular 实现组件间的导航的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Vue写法:
①可以直接修改地址栏(内部测试)
②可以通过js
this.$router.push('目的地的路由地址')
③routerLink
<router-link to="目的地的路由地址"></router-link>
Angular:
①直接修改地址栏
②js
import {Router} from '@angular/router'
constructor(private myRouter:Router){}
this.myRouter.navigateByUrl('url');
③ <a routerLink="地址"></a>

补充:实现前进和后退
import {Location} from '@angular/common'

constructor(private myLocation:Location){}

this.myLocation.back(); this.myLocation.forward();

3、参数的传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Angular:
3.1 发送
this.myRouter.navigateByUrl('myOC/123');

3.2 接收
① 配置接收方的路由地址
{path:'myOC'} ==> {path:'myOC/:price'}
② 接收参数
import {ActivatedRoute} from '@angular/router'

constructor(private myAR:ActivatedRoute){}

this.myAR.params.subscribe((result)=>{
//result.price
})

在Angular中 实现数据传输的方式:
①组件间通信
②跳转时指定参数
③与远程服务器端通信

4、路由嵌套

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
可以在SPA中某个组件中,根据url嵌套其它的组件
Vue中实现方式:
①在准备嵌套其它组件的,指定一个容器 <router-view></router-view>
②配置路由词典
{
path:'',
component:MailComponent,
children:[
{path:'inbox',component:***}
]
}

Angular中实现方式:
①指定容器
router-outlet
②配置子路由
{
path:'mail',
children:[
...
]
}

总结:在Angular中实现一个支持路由嵌套的SPA,
导航到对应的子路由对应的页面时,必须在携带父组件的地址
localhost:3000/mail/outbox
localhost:3000/mail/inbox

demo18_embed
mylogin.component.ts MyLoginComponent
mail.component.ts MailComponent
inbox.component.ts InboxComponent
outbox.component.ts OutboxComponent
①完成组件的创建和声明

②路由模块

5、路由守卫

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
路由守卫 RouteGuard,控制是否能够访问某一个url中所对应的组件!
鉴权的组件
用户登录的页面
。。。

如何使用路由守卫:
①创建一个服务
import {Injectable} from '@angular/core'
import {CanActivate} from '@angular/router'

@Injectable()
export class MailGuard implments CanActivate{

canActivate(){
return true/false
}
}
②给服务指定提供商
providers:[MailGuard]
③给路由词典中想要保护的路由指定canActivate
{
path:'mail',
canActivate:[MailGuard]
}

Vue中如果也想实现路由守卫:
const router = new VueRouter({ ... })

router.beforeEach((to, from, next) => {
// ...
})

https://router.vuejs.org/zh-cn/advanced/navigation-guards.html

一、React 环境配置

安装脚手架工具

1
$ npm install -g create-react-app

创建项目工程

1
2
// demo是项目名称,不能有大写字母
$ create-react-app demo

安装浏览器调试工具

  • 在 FireFox 中安装 React Developer Tools 和 Redux DevTools
  • React Developer Tools 能够在工具栏中很方便的看到页面的布局
  • Redux DevTools 能够追踪页面 state、action 等的变化

二、Redux

Redux 作用

当项目越来越复杂时,组件越来越多的时候,组件之间数据的共享就成为了一个问题,那么 Redux 就是用来解决各个组件之间数据共享的问题的。

在项目中安装 Redux

redux 的 GitHub 地址:https://github.com/reduxjs/redux

1
2
3
// 两种安装方法
$ npm install --save redux
$ yarn add redux

理解 Redux 工作流程

image

  1. Store 就是项目中的数据仓库,但是数据是交给 Reducer 管理的
  2. Store 向组件提供数据,组件订阅数据后,会根据状态的变化自动更新数据
  3. 组件要修改 Store 中的数据,需要创建一个 action 利用 dispatch 函数通知 Store
  4. Store 把 action 和数据交给 Reducer,Reducer 根据 action 的类型来处理数据
  5. Reducer 把处理好的数据返回给 Store

从 Store 中读取数据:派发 action 改变 Store 中的数据

action 是一个字符串,在 App 组件、reducer 文件中都使用了,容易引起难以调试的 Bug,也不利于后期维护。
所以使用一个 actionType 和 actionCreator 来管理 action 的类型和 action 的创建。
代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
// App组件
import React, { Component } from 'react';
import store from './store';
import { changeInputValue } from './store/actionCreators';

class App extends Component {
constructor(props) {
super(props);
this.state = store.getState();
this.handleChangeInput = this.handleChangeInput.bind(this);
this.handleStoreChange = this.handleStoreChange.bind(this);
store.subscribe(this.handleStoreChange);
}

render() {
return (
<div>
<input
placeholder="请输入信息"
style={{width: "200px", height: '40px', border: '1px solid red'}}
value={this.state.inputValue}
onChange={this.handleChangeInput}
/>
</div>
);
}

handleChangeInput (e) {
store.dispatch(changeInputValue(e.target.value));
}

handleStoreChange () {
this.setState(store.getState());
}
}

export default App;

------------------------------

// actionTypes.js 管理action类型
export const CHANGE_INPUT_VALUE = 'change_input_value';

------------------------------

// actionCreators.js 管理action的创建

import { CHANGE_INPUT_VALUE } from './actionTypes';

export const changeInputValue = (value) => ({
type: CHANGE_INPUT_VALUE,
value: value
})

------------------------------

// reducer.js

import { CHANGE_INPUT_VALUE } from './actionTypes';

const defaultState = {
inputValue: 'zhangsan'
}

export default (state = defaultState, action) => {
if (action.type === CHANGE_INPUT_VALUE) {
const newState = JSON.parse(JSON.stringify(state));
newState.inputValue = action.value;
return newState;
}
return state;
}

------------------------------

// store/index.js

import { createStore } from 'redux';
import reducer from './reducer';

const store = createStore(
reducer,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);

export default store;

三、Redux-Thunk

Redux-Thunk 的作用

Redux-Thunk是 Redux的中间件(并不是 React 的中间件),用来将组件异步获取数据的操作封装到 action 中去,以此来减少组件中复杂的异步操作。
使用 Redux-Thunk 之后 action 可以返回一个函数(Redux 的 action 默认只能返回对象)。

安装以及配置

1
2
$ yarn add redux-thunk
$ npm install --save redux-thunk

使用 Redux-Thunk 之后的数据流程

image

实际上变化就在 Dispatch 中,使用 Redux-Thunk 之后 action 不仅可以返回对象,还可以返回函数,然后将异步操作代码放在 action 中了。

代码演示

这里采用从服务端获取数据展示一个 ul 列表

使用 Redux-Thunk 之后的代码以及配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
// App.js
import React, { Component } from 'react';
import store from './store';
import { getListDataAction } from './store/actionCreators';

class App extends Component {

constructor(props) {
super(props);
this.state = store.getState();
this.handleStoreChange = this.handleStoreChange.bind(this);
store.subscribe(this.handleStoreChange);
}

render() {
return (
<div>
<ul>
{this.state.list.map((item, index) => {
return <li key={index}>{item}</li>
})}
</ul>
</div>
);
}

componentDidMount() {
// 着重变化的代码
store.dispatch(getListDataAction())
}

handleStoreChange () {
this.setState(store.getState());
}
}

export default App;

-----------------------

// actionCreators.js
import { GET_LIST_DATA } from './actionTypes';
import axios from 'axios';

export const getListData = (data) => ({
type: GET_LIST_DATA,
value: data
})

// action 返回一个函数,异步操作在这里进行
export const getListDataAction = () => {
return (dispatch) => {
axios.get('/list.json').then((res) => {
if (res.data) {
dispatch(getListData(res.data));
}
}).catch((e) => {
console.log(e);
})
}
}

-----------------------

// store.js
import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import reducer from './reducer';

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose;

const enhancer = composeEnhancers(
applyMiddleware(thunk)
);

const store = createStore(
reducer,
enhancer
);

export default store;

-----------------------

// reducer.js无变化
import { GET_LIST_DATA } from './actionTypes';
const defaultState = {
list: []
}
export default (state = defaultState, action) => {
if (action.type === GET_LIST_DATA) {
const newState = JSON.parse(JSON.stringify(state));
newState.list = action.value;
console.log(action);
return newState;
}
return state;
}

结果:

image

四、Redux-saga

作用

redux-saga 同样是用来拆分组件异步代码的中间件,它和 Redux-Thunk 的区别就是将异步代码的操作放到一个单独的文件中去管理。

安装

1
2
$ npm install --save redux-saga
$ yarn add redux-saga

代码演示

这里采用上面的例子,同样从服务器获取数据展示在 ul 列表中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
// App.js
import React, { Component } from 'react';
import store from './store';
import { getListDataSagas } from './store/actionCreators';

class App extends Component {

constructor(props) {
super(props);
this.state = store.getState();
this.handleStoreChange = this.handleStoreChange.bind(this);
store.subscribe(this.handleStoreChange);
}

render() {
return (
<div>
<ul>
{this.state.list.map((item, index) => {
return <li key={index}>{item}</li>
})}
</ul>
</div>
);
}

componentDidMount() {
store.dispatch(getListDataSagas());
}

handleStoreChange () {
this.setState(store.getState());
}
}

export default App;

------------------------

// actionTypes.js
export const GET_LIST_DATA = 'get_list_data';
export const GET_LIST_DATA_SAGAS = 'get_list_data_sagas';

------------------------

// actionCreators.js
import { GET_LIST_DATA, GET_LIST_DATA_SAGAS } from './actionTypes';

export const getListData = (data) => ({
type: GET_LIST_DATA,
value: data
})

// 这里创建了一个sagas中需要的action
export const getListDataSagas = () => ({
type: GET_LIST_DATA_SAGAS,
})
------------------------

// reducer.js
import { GET_LIST_DATA } from './actionTypes';

const defaultState = {
list: []
}

export default (state = defaultState, action) => {
if (action.type === GET_LIST_DATA) {
const newState = JSON.parse(JSON.stringify(state));
newState.list = action.value;
console.log(action);
return newState;
}
return state;
}
------------------------

// store.js
import { createStore, applyMiddleware, compose } from 'redux';
import reducer from './reducer';
import createSagaMiddleware from 'redux-saga';
// 新创建的sagas.js文件用来管理异步操作的代码
import sagas from './sagas';

const sagaMiddleware = createSagaMiddleware();
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose;
const enhancer = composeEnhancers(
applyMiddleware(sagaMiddleware)
);
const store = createStore(
reducer,
enhancer
);
sagaMiddleware.run(sagas);
export default store;

------------------------
// sagas.js 单独管理异步操作的文件
import { put, takeEvery } from 'redux-saga/effects';
import { GET_LIST_DATA_SAGAS } from './actionTypes';
import axios from 'axios';
import { getListData } from './actionCreators';

function* mySaga() {
// 这句代码意思是:当接收到一个名叫GET_LIST_DATA_SAGAS的action的时候
//会去调用getDataFromServer方法
yield takeEvery(GET_LIST_DATA_SAGAS, getDataFromServer);
}

// 获取数据的异步操作
function* getDataFromServer() {
try {
const res = yield axios.get('/list.json');
yield put(getListData(res.data));
} catch (e) {
console.log('请求错误');
}
}
export default mySaga;

五、React-Redux

作用

React-Redux 的作用是为了在项目中使用 Redux 更加方便。

安装

1
2
$ npm install --save react-redux
$ yarn add react-redux

代码演示

代码演示的例子是改变 input 输入框中的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
// index.js 项目的入口文件
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { Provider } from 'react-redux';
import store from './store';

const AppRoot = (
// 重点把store提供给每一个组件
<Provider store={store}>
<App></App>
</Provider>
);
ReactDOM.render(AppRoot, document.getElementById('root'));

------------------------
// actionTypes.js
export const INPUT_CHANGE = 'input_change';

------------------------
// actionCreators.js
import { INPUT_CHANGE } from './actionTypes';

export const inputChange = (data) => ({
type: INPUT_CHANGE,
value: data
})

------------------------
// reducer.js
import { INPUT_CHANGE } from './actionTypes';
const defaultState = {
inputValue: ''
}
export default (state = defaultState, action) => {
if (action.type === INPUT_CHANGE) {
const newState = JSON.parse(JSON.stringify(state));
newState.inputValue = action.value;
return newState;
}
return state;
}

------------------------
// App.js
import React, { Component } from 'react';
import { inputChange } from './store/actionCreators';
import { connect } from 'react-redux';

class App extends Component {
render() {
return (
<div>
<input
placeholder="请输入信息"
style={{width: "200px", height: '40px', border: '1px solid red'}}
value={this.props.inputValue}
onChange={this.props.handleChangeInput}
/>
</div>
);
}
}

// 将store中存储的数据映射到当前组件的props中
const mapStateToProps = (state) => {
return {
inputValue: state.inputValue
}
}

const mapDispatchToProps = (dispatch) => {
return {
handleChangeInput (e) {
dispatch(inputChange(e.target.value));
}
}
}
// 利用connect将组件和store连接(连接规则mapStateToProps、mapDispatchToProps)
export default connect(mapStateToProps, mapDispatchToProps)(App);

react 推崇的是组件化开发

下面就是一个有两个组件的 demo 演示基本骨架的搭建。

image

六、总结

  • Redux: 就是用来管理项目中状态或者数据的
  • Redux-Thunk: Redux 的中间件,用来将异步操作的代码拆分到 action 中去的
  • Redux-Sagas: Redux 的中间件,用来将异步操作的代码拆分到单独的文件中管理
  • React-Redux: 更能方便的管理和使用 Redux

一、新的组件生命周期钩子

image

React 的生命周期钩子允许开发者在组件实例化、完成渲染、属性、更新、销毁等不同阶段操作组件。这给我们的开发带来更多的灵活性;

但是,如果开发者对各个生命周期的理解错误,很容易造成对它的滥用,就会造成性能上的浪费等问题。

比如:componentWillMount 对组件进行 setState、dom 操作、添加事件监听、获取数据等。这些都是不安全的操作。

react16 对组件的生命周期做了优化,”移除”容易被大家误解的钩子,添加更加容易理解、安全的钩子。

“删除”了以下钩子(17 版本中将真的删除):

componentWillMount()

componentWillReceiveProps(nextProps, nextState)

componentWillUpdate()

新增了以下钩子:

static getDerivedStateFromProps(nextProps, prevState)

getSnapshotBeforeUpdate(prevProps, prevState)

componentDidCatch(error, info)

a. 如何理解 static getDerivedStateFromProps(nextProps, prevState)?

首先,需要对静态方法做一个理解。static 静态方法,在 es5 中怎么实现呢?
function Person() {}
Person.getCount = function () {}
以上就是 static 静态方法的原理。由于“this”只能获取属性是根据原型链,而静态方法不在原型链上,所以,在组件实例内无法通过 this 调用 static 方法,static 方法也无法根据”this”调用实例的其他方法。
就防止在 getDerivedStateFromProps 对组件实例的错误操作。
再次,getDerivedStateFromProps 用来做什么用呢?
当组件实例化、接收到新的 props 时,会调用该方法。方法返回一个对象,这个对象会被更新到组件的 state 上。如果返回空,那么不对 state 做更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 以下代码实现,更新name属性到state上;
static getDerivedStateFromProps (nextProps, prevState) {
return {
name: nextProps.name
};
}

// 上面的代码在以前版本中
// 你可能会用以下这样做,虽然这样做看起来也没问题,用上面的方法更加安全,不会对this做误操作
componentWillReceiveProps (nextProps) {
if (this.state.name !== nextProps.name) {
this.setState({
name: nextProps.name
});
}
}

使用

这个生命周期函数是为了替代 componentWillReceiveProps 存在的,所以在你需要使用 componentWillReceiveProps 的时候,就可以考虑使用 getDerivedStateFromProps 来进行替代了。

两者的参数是不相同的,而 getDerivedStateFromProps 是一个静态函数,也就是这个函数不能通过 this 访问到 class 的属性,也并不推荐直接访问属性。而是应该通过参数提供的 nextProps 以及 prevState 来进行判断,根据新传入的 props 来映射到 state。

需要注意的是,如果 props 传入的内容不需要影响到你的 state,那么就需要返回一个 null,这个返回值是必须的,所以尽量将其写到函数的末尾。

1
2
3
4
5
6
7
8
9
10
11
static getDerivedStateFromProps(nextProps, prevState) {
const {type} = nextProps;
// 当传入的type发生变化的时候,更新state
if (type !== prevState.type) {
return {
type,
};
}
// 否则,对于state不进行任何操作
return null;
}

b. 如何理解 getSnapshotBeforeUpdate(prevProps, prevState)?

首先,从字面来理解“snapshot”是快照的意思。在 dom 更新之前调用。返回的值将被传给 componentDidUpdate(prevProps, prevState, snaphot)。
这个会比较少用到,但对于处理比如数据更新后的滚动条的差异滚动,对用户体验,很有帮助。

c. 如何理解 componentDidCatch(error, info)?

以往,当组件发生错误(可以用 throw new Error 模拟)时,会导致整个 react 程序死掉,这对于程序的稳定性来说非常不好。
componentDidCatch 可以捕获子组件中任何一个错误,捕获到错误后可以对错误进行处理。
如果发生错误的组件的父组件没有设置 componentDidCatch 错误捕获,将继续递归父组件的父组件中的 componentDidCatch,找到则停止。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 简单的错误捕获
componentDidCatch (error, info) {
this.setState({
error: true
});
}

render () {
if (this.state.error) {
return <div>子组件发生异常了</div>
}

// 其他代码
}

二、优化了哪些语法

1. ref 优化

ref 有很多作用,通过 ref 父组件可以调用子组件内的方法,配合ReactDOM.findDOMNode(ref) 可以获取到组件对应的 dom。ref 与 key 一样无法通过 this.props.ref 获取;

以前版本的 react,给子组件添加 ref=“inputName”,就可以通过 this.refs[‘inputName’]获取子组件实例。然后可以进行一些操作。

React16 中有两种创建 Ref 的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
constructor () {
this.inputNameRef = React.createRef();
this.switchRef = React.createRef();
}

render () {
// 通过this.inputNameRef.current 可以获取到input实例
return (
<div>
<input ref={this.inputNameRef} />
<Switch ref={this.switchRef} />
</div>
)
}
1
2
3
4
5
6
7
8
9
10
11
render () {
// 通过回调ref的方式实现
// 通过this.inputNameRef 可以获取到input实例
// this.switchRef可以获取Switch的实例
return (
<div>
<input ref={(ref) => this.inputNameRef = ref} />
<Switch ref={(ref) => this.switchRef = ref} />
</div>
)
}

那么,既然 ref 和 key 可以一样不能用 this.props 获取,有没有办法传给子组件呢?这样就可以实现在组件中调用子子组件(比如子组件中的 input)了。

答案是肯定的。

也有两种方法:

1
2
3
4
5
6
7
8
render () {
// 假设this.switchRef已经在constructor里创建了,那么可以通过其他属性传递。
// 在子组件中可以通过this.props.forRef。
// 注:forRef 为随便名只要不是react内置的名称就行
return (
<Switch forRef={this.switchRef} />
);
}
1
2
3
4
5
6
7
8
9
10
// 通过React.forwardRef 传递
export default React.forwardRef((props, ref) => {
return (
<div>
<OtherChild />
<Switch ref={ref} />
<OtherChild2 />
</div>
)
});

2. 将组件实例化到其他 dom 下,可以优化吗?

React 渲染时,默认是将节点渲染到父组件中,这样能满足我们大部分的需求,but,有这样的组件,比如 Dialog,在组件渲染时,需要能定义组件弹出的 dom 位置,如 Dialog 中的 appendToBody 为 true 时,渲染到 body 节点下。

以前我们的做法是,通过 ref 和 ReactDOM.findDomNode 获取 dom 在 dialog open 时通过 document.body.appendChild(dom),将 append 到 body 下,在 componentWillUnmount 时,document.body.removeChild(dom),移除没用的 dom。

这样来做就有点麻烦,需要 ref、ReactDOM 等。

React16 提供了一种优雅的方式 createPortal,Portals 提供了一种很好的将子节点渲染到父组件以外的 DOM 节点的方式。

createPortal,渲染子节点到目标节点,并返回元素。

我们知道 ReactDOM.render 也可以将子节点渲染到目标节点,区别是 ReactDOM.render 并没有元素,因此不作为 render 方法的 return

我直接贴 react 的 Demo。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const modalRoot = document.body;

class Modal extends React.Component {
constructor(props) {
super(props);
this.el = document.createElement('div');
}

componentDidMount() {
modalRoot.appendChild(this.el);
}

componentWillUnmount() {
modalRoot.removeChild(this.el);
}

render() {
return ReactDOM.createPortal(
this.props.children,
this.el,
);
}
}

3. 需要传递的多层的 props,可以优化吗?

在实际开发中,我们经常会碰到,一个值需要被传递到多层的子组件中,我们不得不通过 props 一层一层的传递。

react16 同样提供了优雅的方式。

React.createContext

三、性能方面的优化

1. React.Fragment

React 渲染时,要求 JSX 元素是 tree 结构,因为 JSX 原理还是调用 React.createElement(Component, attrs, [...children]),这样好理解。但这样在组件开发过程中,就有所限制,比如:Table Tr 组件,需要渲染同时多个 Tr,在以前我们就需要包一层<div>,造成了浪费。

react16 提供了Fragment,可以让我们渲染一个子元素列表,并且不在 DOM 中增加额外节点。

1
2
3
4
5
6
7
8
render () {
return (
<> // 也可以用 <React.Fragment>
<tr><td>1</td></tr>
<tr><td>1</td></tr>
</> // 也可以用 </React.Fragment>
);
}

2. React.memo

用在函数组件的性能优化上,会对函数组件实现和 PureComponent 一样的功能,对 props 和 prevProps 做一次 shallowEqual 浅比较。

用法:

1
2
3
React.memo((props) => {
// codes
});

3.React.lazy & React.Suspense

实现 React 的 Code Spliting 对 React 代码进行拆包,有效的减少一次性加载太多代码的问题。

用法:

1
2
3
4
5
6
const IconDemo = lazy(() => import('./icon'));

<Suspense fallback={<div className="loading">Loading...</div>}>
<Route path="/" component={Home} exact />
<Route path="/icon" component={IconDemo} />
</Suspense>

四、hooks

image

我们知道,React 创建组件有 3 中方式【extends React.Component / PureComponent】、【React.createClass】,纯函数(无副作用,只接受参数,不对参数做任何处理)。

extends React.Component、React.createClass 创建的组件,拥有完成 react 声明周期、状态,可以开发较为复杂的组件。

而,纯函数组件,只能做渲染,绑定的属性都是通过 props 决定的。没有声明周期、状态的概念。如下:

1
2
3
4
5
6
function UserInfo (props) {
// 这里可以写一些操作
return (
<div>{this.props.name}</div>
)
}

React v16.7.0-alpha 对函数组件做了以下扩展。

a. 针对没有 state 做的扩展

useState,可以将 state 添加到函数组件中,允许函数组件操作 state。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import {useState} from 'react';

function UserInfo(props) {
// useState 接收一个参数(当做初始的state)
// 返回一个初始state值,和修改state的方法
// 允许多次使用,因此传给useState的参数应该尽可能是简单的数字、字符串等。
let [count, setCount] = useState(0);
let [checked, setChecked] = useState(props.checked || false);

return (
<div>
<p onClick={() => setCount(count + 1)}>Clicked {count} times</p>
<Switch onClick={() => setChecked(!checked)}>{ checked ? '开' : '关' }</Switch>
</div>
)
}

b. 针对没有生命周期钩子的扩展

useEffect,可以将 componentDidMount、componentDidUpdate、componentWillUnmount 钩子添加到函数组件中。

注意:区分有返回值,和没有返回值的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import {useState, useEffect} from 'react';

function UserInfo(props) {
let [count, setCount] = useState(0);
let [checked, setChecked] = useState(props.checked || false);

function handleStatusChange(checked) {
setChecked(checked);
}

// 返回值的情况
useEffect(() => {
// 组件Mount是添加监听,componentDidMount
// subscribe原理是,callbacks.push(handleStatusChange)
// 当值status值变化是,对遍历callbacks数组,调用内方法
// callbacks.map((fn) => { fn(status) });
ChatAPI.subscribeToFriendStatus(props.id, handleStatusChange);

// 组件销毁是移除监听(componentWillUnmount,时执行return的内方法)
// unsubscribe的原理是
// let index = callbacks.indexOf(fn);
// callbacks.splice(index, 1);
return function cleanup() {
ChatAPI.unsubscribeFromFriendStatus(props.id, handleStatusChange);
};
});


// 没有返回值的情况
useEffect(() => {
// 组件Mount是调用,componentDidMount、componentDidUpdate1
document.title = `Clicked ${count}`;
});

return (
<div>
<p onClick={() => setCount(count + 1)}>Clicked {count} times</p>
<Switch onClick={() => setChecked(!checked)}>{ checked ? '开' : '关' }</Switch>

</div>
)
}

c. one more thing

出于不至于滥用的目的,react 指定了一些 hooks 的使用规则。如命名、代码位置等。当然还很友好的除了代码检测 eslint 插件(eslint-plugin-react-hooks),可以检测开发者写的 hooks 是否合法。

规则如下:

  • 在函数组件的顶层使用 hooks,不要在函数组件内的 for、if、匿名函数内使用 useState、useEffect,这将会报错。
  • 关于 hooks 组件的使用,你可以在 react 组件内使用,也可以在 custom Hooks 内使用。
    什么是自定义 hook(custom Hooks)呢?就是对 hook 的一个封装,我们可以封装特定功能的 hook,比如:用户状态的 hook、在线人数的 hook,这样在其他地方也可以使用。建议以 useXxxxx 的规范命名。

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
function useSetState (initial) {
const [state, set] = useState(initial);
const setState = (patch) => {
if (patch instanceof Function) {
set((prevState) => {
return Object.assign(state, patch(prevState));
});
} else {
Object.assign(state, patch);
set(state);
}
};

return [state, setState];
}

// 调用
const Demo = () => {
const [state, setState] = useSetState({});

return (
<div>
<div>{ JSON.stringify(state, null, 2) }</div>
<button onClick={() => setState({foo: 'bar'})}>foo</button>
<button
onClick={() => {
setState((prevState) => ({
count: (prevState.count || 0) + 1,
}));
}}
>
count
</button>
</div>
);
};