【CuteJavaScript】GraphQL真香入门教程
看完复联四,我整理了这份 GraphQL 入门教程,哈哈真香。。。
欢迎关注我的 个人主页 && 个人博客 && 个人知识库 && 微信公众号“前端自习课”
首先有请阿爸镇贴!哈哈哈,需要高清原图的小伙伴可以 点我下载 阿爸无敌 。
下面开始本文内容:
一、GraphQL介绍
GraphQL
是 Facebook 开发的一种 API 的查询语言,与 2015 年公开发布,是 REST API 的替代品。
GraphQL
既是一种用于 API 的查询语言也是一个满足你数据查询的运行时。 GraphQL
对你的 API 中的数据提供了一套易于理解的完整描述,使得客户端能够准确地获得它需要的数据,而且没有任何冗余,也让 API 更容易地随着时间推移而演进,还能用于构建强大的开发者工具。
官网: http://graphql.org/
中文网: http://graphql.cn/
1. 特点
- 请求你所要的数据,不多不少;
如:hero
中有 name
, age
, sex
等,可以只取得需要的字段。
- 获取多个资源,只用一个请求;
典型的 REST API
请求多个资源时得载入多个 URL,而 GraphQL
可以通过一次请求就获取你应用所需的所有数据。这样也能保证在较慢的移动网络连接下,使用 GraphQL
的应用也能表现得足够迅速。
- 描述所有可能类型的系统。便于维护,根据需求平滑演进,添加或隐藏字段;
GraphQL
使用类型来保证应用只请求可能的数据,还提供了清晰的辅助性错误信息。应用可以使用类型,而避免编写手动解析代码。
2. 简单案例
这里先看下简单案例,体验下 GraphQL
的神奇之处(后面详细介绍)。
我们这样定义查询语句:
query {hero
}
然后得到的就是我们所要查询的 hero
字段:
{"data": {"hero": "I'm iron man"}
}
这样用起来,是不是更舒服呢?
二、GraphQL与restful对比
1. restful介绍
全称:Representational State Transfer
表属性状态转移。
本质上就是定义 uri ,通过 API 接口来取得资源。通用系统架构,不受语言限制。
例子: 饿了吗接口。
如:接口 restapi/shopping/v3/restaurants?latitude=13
就是个典型的 restful
接口,定义资源 + 查询条件。
2. 与 GraphQL 比较
restful
一个接口只能返回一个资源,GraphQL
一次可以获取多个资源。restful
用不同 url 来区分资源,GraphQL
用类型区分资源。
三、使用express构建基本helloworld
1. 简单案例
首先创建一个文件夹 demo
,并初始化一个 package.json
,安装 express
/ graphql
/ express-graphql
依赖包:
npm init -y
npm install express graphql express-graphql -S
新建一个 hello.js
,引入文件:
const express = require('express')
const { buildSchema } = require('graphql')
const graphqlHTTP = require('express-graphql')
创建一个 schema
来定义查询语句和类型,buildSchema()
方法需要传入的参数是字符串类型,如下面的 hero
查询字段,后面的 String
类型表示字段返回的数据类型:
const schema = buildSchema(`type Query {hero: String}
`)
创建一个 root
处理器,处理对应的查询,这里的 hello
处理器对应的是 schema
中的 hero
字段查询的处理,这里直接返回 I'm iron man
的结果:
const root = {hero: () => {return "I'm iron man"}
}
当然,处理器中也可以是其他复杂操作,后面会介绍。
然后实例化 express
,并且将路由转发给 graphqlHTTP
处理:
const app = express()
app.use('/graphql', graphqlHTTP({schema,rootValue: root,graphiql: true
}))
app.listen(3000)
graphqlHTTP
中的三个参数介绍:
schema
:定义的查询语句和类型rootValue
:处理对应查询的处理器graphiql
:是否开启调试窗口,开发阶段开启,生产阶段关闭
接下来运行项目,在命令行中执行 node hello.js
,这里可以在 graphiql
上做调试,打开地址 localhost:3000/graphiql
就可以愉快的查询了。
另外我们可以在 graphiql
界面右侧打开 Docs 查看我们定义的所有字段和描述信息。
最终代码:
const express = require('express')
const { buildSchema } = require('graphql')
const graphqlHTTP = require('express-graphql')// 构建schema,这里定义查询的语句和类型
const schema = buildSchema(`type Query {hero: String}
`)// 定义查询所对应的 resolver,也就是查询对应的处理器
const root = {hero: () => {return "I'm iron man"}
}const app = express()// 将路由转发给 graphqlHTTP 处理
app.use('/graphql', graphqlHTTP({schema: schema,rootValue: root,graphiql: true
}))app.listen(3000)
2. 自定义类型查询
我们前面的查询中,已经将 hero
字段定义为 String
类型,但是常常开发中,我们又会碰到字段是多个类型,即字段也能指代对象类型(Object),比如一个 user
字段会有 name
、age
等字段,而 name
返回字符串类型,age
返回数值类型。
这时候,我们可以对这个对象的字段进行次级选择(sub-selection)。GraphQL 查询能够遍历相关对象及其字段,使得客户端可以一次请求查询大量相关数据,而不像传统 REST 架构中那样需要多次往返查询。
我们可以新建一个查询类型来定义 user
字段返回的类型:
const schema = buildSchema(`type User {# 查询可以有备注!name: Stringage: Int}type Query {hero: Stringuser: User}
`)
在处理器中我们也要加上:
const root = {hero: () => {return "I'm iron man"},user: () => {return {name: 'leo',age: 18}}
}
这边 Int/String 参数类型的问题,下一章介绍
四、参数类型和参数传递
1. 基本参数类型
String
, Int
, Float
, Boolean
和 ID
,这些基本参数类型可以在 schema
声明中直接使用。
Int
:有符号 32 位整数。Float
:有符号双精度浮点值。String
:UTF‐8
字符序列。Boolean
:true
或者false
。ID
:ID
标量类型表示一个唯一标识符,通常用以重新获取对象或者作为缓存中的键。ID
类型使用和String
一样的方式序列化;然而将其定义为ID
意味着并不需要人类可读型。
另外,我们可以使用 [类型]
来表示一类数组,如:
[Int]
表示整型数组;[String]
表示字符串型数组;
2. 参数传递
使用方式和 JS 参数传递一样,小括号内定义形参,但是参数需要定义类型。
使用 !
代表参数不能为空。
下面案例:参数 teamName
是 String
类型,必须传递,而 number
参数也是 Int
类型,但是是非必须传递,最后输出的结果也是 String
类型。
type Query {getHero(teamName: String!, number: Int): [String]
}
下面一个案例:
//...省略其他
const schema = buildSchema(`type Query {getHero(teamName: String!): [String]}
`)const root = {getHero: ({teamName}) => {// 这里的操作 实际开发中常常用在请求数据库const hero = {'三国': ['张飞', '刘备', '关羽'],'复仇者联盟': ['钢铁侠', '美国队长', '绿巨人']}return hero[teamName]}
}
//...省略其他
这时候我们在 GraphiQL 上输入查询,就会得到 复仇者联盟 的英雄数据了。
// 查询
query {getHero(teamName:"复仇者联盟")
}// 结果
{"data": {"getHero": ["钢铁侠","美国队长","绿巨人"]}
}
3. 自定义返回类型
在实际开发中,我们返回的数据类型可能是一个对象,对象中可能既有 Int
类型的属性,也有 String
类型的值,等等,这里我们可以使用 自定义返回类型 来处理:
//...省略其他
const schema = buildSchema(`type Hero {name: Stringage: IntdoSomething(thing: String): String}type Query {getSuperHero(heroName: String!): Hero}
`)
const root = {getSuperHero: ({heroName}) => {// 这里的操作 实际开发中常常用在请求数据库const name = heroNameconst age = 18const doSomething = ({thing}) => {return `I'm ${name}, I'm ${thing} now`}return { name, age, doSomething }}
}
//...省略其他
这里指定了 getSuperHero
字段的返回类型是 Hero
类型,随后在上面定义了 Hero
。
其中 Hero
类型中的 doSomething
也是可以传递指定类型参数,并且指定返回类型。
下面看下输出情况:
// 查询
query {getSuperHero(heroName:"IronMan") {nameagedoSomething}
}// 结果
{"data": {"getSuperHero": {"name": "IronMan","age": 46,"doSomething": "I'm IronMan, I'm undefined now"}}
}
这里也可以给 doSomething
传递参数,就会获取到不同结果:
// 查询
query {getSuperHero(heroName:"IronMan") {nameagedoSomething(thing:"watching TV")}
}// 结果
{"data": {"getSuperHero": {"name": "IronMan","age": 46,"doSomething": "I'm IronMan, I'm watching TV now"}}
}
五、GraphQL客户端
这一节我们学习如何在客户端中访问 graphql
的接口。
1. 后端定义接口
我们先在后端将接口开发完成,这里跟前面差不多,但需要多一步,使用 express
向外暴露一个文件夹,供用户访问静态资源文件:
这里直接使用前一节的代码啦~
// index.js 开发 graphql 接口
//...省略其他
const schema = buildSchema(`type Hero {name: Stringage: IntdoSomething(thing: String): String}type Query {getSuperHero(heroName: String!): Hero}
`)
const root = {getSuperHero: ({heroName}) => {// 这里的操作 实际开发中常常用在请求数据库const name = heroNameconst age = 46const doSomething = ({thing}) => {return `I'm ${name}, I'm ${thing} now`}return { name, age, doSomething }}
}
const app = express()
app.use('/graphql', graphqlHTTP({schema, rootValue: root, graphiql: true
}))
// 公开文件夹 使用户访问静态资源
app.use(express.static('public'))
app.listen(3000)
这样我们就给前端页面提供一个可以访问静态资源的功能。
这里还需要在根目录创建一个 public 文件夹,并在文件夹中添加 index.html
文件,此时的目录结构:
|-node_modules
|-public
|---index.html
|-index.js
|-package.json
2. 前端页面请求
然后给 index.html
添加按钮和事件绑定:
这里的变量 query
是个字符串类型,定义查询条件,在条件 GetSuperHero
中的参数,需要用 $
符号来标识,并在实际查询 getSuperHero
中,作为参数的参数类型设置进来。
然后定义变量 variables
,指定属性的值,之后通过 fetch
发起请求:
<button onclick="getData()">获取数据</button>
<script>
function getData(){const query = `query GetSuperHero($heroName: String, $thing: String){getSuperHero(heroName: $heroName){nameagedoSomething(thing: $thing)}}`// 如果不需要其他参数 至少要传一个参数 否则会报错// const query = `// query GetSuperHero($heroName: String){// getSuperHero(heroName: $heroName){// name// }// }// ` const variables = {heroName: '钢铁侠', thing: 'watching TV'}fetch('./graphql', {method: 'POST',headers: {'Content-Type': 'application/json','Accept': 'application/json'},body: JSON.stringify({query, variables})}).then(res => res.json()).then(json => {console.log(json)})
}
</script>
当我们写完以后,点击 获取数据 就会在控制台打印下面的数据:
{"data":{"getSuperHero":{"name":"钢铁侠","age":46,"doSomething": "I'm 钢铁侠, I'm watching TV now"}}
}
3. 注意点
- 请求中的
query
参数需要对照好有$
符号的变量。
查询语句 query GetSuperHero($heroName: String)
里参数 $heroName
中的 heroName
;
查询语句 getSuperHero(heroName: $heroName)
里类型 $heroName
中的 heroName
;
变量 variables
中的 heroName
属性;
这三个名称需要一样。
- 请求中需要将数据序列化操作。
body: JSON.stringify({ query, variables })
六、使用Mutations修改数据
1. Mutation 使用
根据前面的学习,我们知道,要做查询操作,需要使用 Query
来声明:
type Query {queryHero(heroName: String): String
}
当我们要做修改操作,需要用到的是 Mutation
:
type Mutation {createHero(heroName: String): String
}
如果 Mutation
中字段的形参是自定义类型,则类型需要用 input
标识:
const schema = buildSchema(`# 输入类型 用 input 标识input HeroInput {name: Stringage: Int}# 查询类型type Hero {name: Stringage: Int}type Mutation {createHero(heroName: String): HeroupdateHero(heroName: String, hero: HeroInput): Hero}
`)
注意下:这里需要至少定义一个 Query
不然GraphiQL
会不显示查询:
type Query {hero: [Hero]
}
2. Mutation 使用案例
先创建一个 schema
,内容为上一步【1. Mutation 使用】中定义的内容,这里不重复写。
然后模拟创建一个本地数据库 localDb
, 用于模拟存放添加的超级英雄数据:
const localDb = {}
接下来声明 root
实现 schema
中的字段方法:
const root = {hero() {// 这里需要转成数组 因为前面定义了返回值是 [Hero] 类型let arr = []for(const key in localDb){arr.push(localDb[key])}return arr},createHero({ input }) {// 相当于数据库的添加操作localDb[input.name] = inputreturn localDb[input.name]},updateHero({ id, input }) {// 相当于数据库的更新操作const update = Object.assign({}, localDb[id], input)localDb[id] = updatereturn update}
}
最后配置 graphqlHTTP
方法和启动服务器,这里就不多重复咯。
最终代码:
//...省略其他
const schema = buildSchema(`# 输入类型 用 input 标识input HeroInput {name: Stringage: Int}# 查询类型type Hero {name: Stringage: Int}type Mutation {createHero(input: HeroInput): Hero updateHero(id: ID!, input: HeroInput): Hero}# 需要至少定义一个 Query 不要GraphiQL会不显示查询type Query {hero: [Hero]}
`)const localDb = {}const root = {hero() {// 这里需要转成数组 因为前面定义了返回值是 [Hero] 类型let arr = []for(const key in localDb){arr.push(localDb[key])}return arr},createHero({ input }) {// 相当于数据库的添加操作localDb[input.name] = inputreturn localDb[input.name]},updateHero({ id, input }) {// 相当于数据库的更新操作const update = Object.assign({}, localDb[id], input)localDb[id] = updatereturn update}
}
//...省略其他
现在我们可以启动服务器,在 GraphiQL
上测试下效果了。
我们是使用 mutation
的 createHero
字段添加两条数据:
mutation {createHero(input: {name: "钢铁侠"age: 40}){nameage}
}
mutation {createHero(input: {name: "美国队长"age: 41}){nameage}
}
然后使用 query
的 hero
字段查询添加的结果:
query {hero {nameage}
}
这样我们就获取到刚才的添加结果:
{"data": {"hero": [{"name": "钢铁侠","age": 40},{"name": "美国队长","age": 41}]}
}
然后我们开始更新数据,使用 mutation
的 updateHero
字段将 美国队长 的 age
值修改为 18:
mutation {updateHero(id: "美国队长", input: {age: 18}){age}
}
再使用 query
的 hero
字段查询下新的数据,会发现 美国队长 的 age
值已经更新为 18:
{"data": {"hero": [{"name": "钢铁侠","age": 40},{"name": "美国队长","age": 18}]}
}
七、认证和中间件
我们知道,修改数据的接口不能让所有人随意访问,所以需要添加权限认证,让有权限的人才可以访问。
在 express
中,可以很简单的使用中间件来将请求进行拦截,将没有权限的请求过滤并返回错误提示。
中间件实际上是一个函数,在接口执行之前,先拦截请求,再决定我们是否接着往下走,还是返回错误提示。
这在【六、使用Mutations修改数据】的最终代码上,在添加这个中间件:
//... 省略其他
const app = express()
const middleWare = (req, res, next) => {// 这里是简单模拟权限// 实际开发中 更多的是和后端进行 token 交换来判断权限if(req.url.indexOf('/graphql') !== -1 && req.headers.cookie.indexOf('auth') === -1){// 向客户端返回一个错误信息res.send(JSON.stringify({err: '暂无权限'}))return}next() // 正常下一步
}
// 注册中间件
app.use(middleWare)//... 省略其他
这里的权限判断,只是简单模拟,实际开发中,更多的是和后端进行 token
交换来判断权限(或者其他形式)。
我们重启服务器,打开 http://localhost:3000/graphql
,发现页面提示错误了,因为 cookies
中没有含有 auth
字符串。
如果这里提示 TypeError: Cannot read property 'indexOf' of undefined
,可以先不用管,因为浏览器中没有 cookies
的原因,其实前面的权限判断逻辑需要根据具体业务场景判断。
为了方便测试,我们在 chrome 浏览器控制台的 application
下,手动设置一个含有 auth
字符串的一个 cookies
,只是测试使用哦。
设置完成后,我们就能正常进入页面。
八、ConstructingTypes
在前面的介绍中,我们要创建一个 schema
都是使用 buildSchema
方法来定义,但我们也可以使用另外一种定义方式。
就是这里要学习使用的构造函数 graphql.GraphQLObjectType
定义,它有这么几个优点和缺点:
- 优点:报错提醒更直观,结构更清晰,更便于维护。
- 缺点:代码量上升。
1. 定义type(类型)
这里先将前面定义的 Hero
类型进行改造:
const graphql = require('graphql') // 需要引入
const HeroType = new graphql.GraphQLObjectType({name: 'Hero',fields: {name:{ type: graphql.GraphQLString },age:{ type: graphql.GraphQLInt },}
})
两者区别在于:
区别 |
buildSchema
|
graphql.GraphQLObjectType
|
---|---|---|
参数类型 | 字符串 | 对象 |
类名 |
跟在 type 字符后面,这里是 type Hero
|
在参数对象的 name 属性上
|
属性定义 | 定义在类型后,键值对形式 |
定义在参数对象 fields 属性中,值为对象,每个属性名为键名,值也是对象,其中 type 属性的值为 graphql 中的属性,下面会补充
|
补充:
fields
属性中的子属性的类型通常有:
graphql.GraphQLString
graphql.GraphQLInt
graphql.GraphQLBoolean
…
即在 GraphQL
后面跟上基本类型名称。
2. 定义query(查询)
定义查询的时候,跟之前类似,可以参照下面对比图理解,这里比较不同的是,多了个 resolve
的方法,这个方法是用来执行处理查询的逻辑,其实就是之前的 root
中的方法。
const QueryType = new graphql.GraphQLObjectType({name: 'Query',fields: {// 一个个查询方法getSuperHero: {type: HeroType,args: {heroName: { type: graphql.GraphQLString }},// 方法实现 查询的处理函数resolve: function(_, { heroName }){const name = heroNameconst age = 18return { name, age }}}}
})
3. 创建 schema
创建的时候只需实例化并且将参数传入即可:
// step3 构造 schema
const schema = new graphql.GraphQLSchema({ query: QueryType})
最后使用也是和前面一样:
const app = express()app.use('/graphql', graphqlHTTP({schema,graphiql: true
}))
app.listen(3000)
九、与数据库结合实战
我们试着使用前面所学的内容,开发一个简单的实践项目:
通过 GraphiQL
页面,往 Mongodb
中插入和更新数据,主要用到【六、使用Mutations修改数据】章节的操作。
1. 搭建并启动本地 Mongodb 数据库
首先我们可以到 Mongodb 官网 选择对应平台和版本下载安装。
下载安装步骤,可以参考 mongoDB下载、安装和配置,这里就不多介绍哟~~
安装完成后,我们打开两个终端,分别执行下面两行命令:
// 终端1 启动数据库
mongod --dbpath c:\leo\app\mongodb\data\db// 终端2 进入数据库命令行操作模式
mongo
2. 连接数据库,创建 Schema 和 Model
首先我们新建一个文件 db.js
,并 npm install mongoose
安装 mongoose
,然后写入下面代码,实现连接数据库:
const express = require('express')
const { buildSchema } = require('graphql')
const graphqlHTTP = require('express-graphql')
const mongoose = require('mongoose')const DB_PATH = 'mongodb://127.0.0.1:27017/hero_table'
const connect = () => {// 连接数据库mongoose.connect(DB_PATH)// 连接断开mongoose.connection.on('disconnected', () => {mongoose.connect(DB_PATH)})// 连接失败mongoose.connection.on('error', err => {console.error(err)})// 连接成功mongoose.connection.on('connected', async () => {console.log('Connected to MongoDB connected', DB_PATH)})
}
connect()
然后创建 Schema
和 Model
:
let HeroSchema = new mongoose.Schema({name: String,age: Number
})
let HeroModel = mongoose.model('hero',HeroSchema, 'hero_table')
3. 声明查询语句
这一步,还是先使用【六、使用Mutations修改数据】章节的操作逻辑,也就是先用字符串创建查询,而不使用 GraphQLObjectType
创建:
const schema = buildSchema(`# 输入类型 用 input 标识input HeroInput {name: Stringage: Int}# 查询类型type Hero {name: Stringage: Int}type Mutation {createHero(input: HeroInput): Hero updateHero(hero: String!, input: HeroInput): Hero}# 需要至少定义一个 Query 不要GraphiQL会不显示查询type Query {hero: [Hero]}
`)
这边案例有稍作修改
4. 实现添加数据和更新数据的逻辑
这边处理添加数据和更新数据的逻辑,就要修改之前声明的 root
的操作内容了:
const root = {hero() {return new Promise( (resolve, reject) => {HeroModel.find((err, res) => {if(err) {reject(err)return}resolve(res)})})},createHero({ input }) {// 实例化一个Modelconst { name, age } = inputconst params = new HeroModel({ name, age })return new Promise( (resolve, reject) => {params.save((err, res) => {if(err) {reject(err)return}resolve(res)})})},updateHero({ hero, input }) {const { age } = inputreturn new Promise ((resolve, reject) => {HeroModel.update({name: hero}, {age}, (err, res) => {if(err) {reject(err)return}resolve(res)})})}
}
5. 模拟测试
最后我们在 GraphiQL
页面上模拟测试一下,首先添加两个英雄,钢铁侠和美国队长,并设置他们的 age / name
属性:
mutation {createHero(input: {name: "钢铁侠"age: 40}){nameage}
}
mutation {createHero(input: {name: "美国队长"age: 20}){nameage}
}
页面和接口没有报错,说明我们添加成功,数据库中也有这两条数据了:
在测试下查询:
query {hero {nameage}
}
查询也正常,接下来测试下更新,将美国队长的 age
修改为 60:
mutation {updateHero(hero: "美国队长", input: {age: 60}){age}
}
到这一步,我们也算是将这个练习做完了。
总结
GraphQL
是一种 API 的查询语言,是 REST API 的替代品。GraphQL
可以使用一个请求,获取所有想要的数据。创建查询的方式有两种:使用
buildSchema
或者GraphQLObjectType
。查询操作用
Query
,修改操作用Mutations
。查询类型用
type
,输入类型用input
。
其实 GraphQL
还是很简单好用的呢~~~
本文首发在 pingan8787个人博客,如需转载请保留个人介绍
【CuteJavaScript】GraphQL真香入门教程相关推荐
- 老徐教你学C语言(C语言进门教程)
都说C语言是所有语言的根基--得C语言者得天下,为了更好的方便初学者入门,特编辑了以下C语言进门教程--老徐教你学C语言,希望对初学者有所帮助 00.老徐教你学C语言--C语言学习之道 ...
- xcode5真机调试教程 xcode5真机测试步骤
http://www.33lc.com/article/31321.html xcode5真机调试教程还有很多朋友不是很了解,下文将会演示xcode5真机测试步骤,如果你想要了解关于xcode5 io ...
- 14门教程带你全面入门Linux
Linux有很多优点:安全.自主.开源--,也正是这些优点使得很多人都在学Linux. 虽说网上有大把的Linux课程资源,但是对很多小白来说网上的课程资源比较零散并不适合新手学习. 正因为此,总结了 ...
- 最全的iOS真机调试教程(证书生成等)
准备 开发者账号 自从Xcode7 出来之后,一般的真机测试不需要开发者账号,也就不需要看这篇教程,只有app具有"推送"等功能的时候,要真机测试就必须要开发者账号和设置证书.苹果 ...
- iOS- 最全的真机测试教程
想要上架的同学请看:<iOS-最全的App上架教程> 因为最近更新了Xcode 8 ,证书的创建都大同小异,只是在Xcode 8中的设置有一些变化,我就在下面补充,如有什么疑问,请联系我. ...
- iOS APP免费开发账号真机调试教程
之前上架苹果APP或者测试苹果APP都需要付费99美金(688元)加入苹果开发者才能进行,但现在真机调试测试完全可以免费进行;因为Appuploader新增加了一个强大的功能 不用苹果付费99美金的开 ...
- 手把手Android Studio全套安装+配置+真机部署教程【多图超详细】
目录 一.Android Studio安装教程 二.Android项目的创建 三.Android Studio下安装部署真机 Hello,你好呀,我是灰小猿,一个超会写bug的程序猿, 今天在这里和大 ...
- 干掉 RESTful!GraphQL 真香!
- 前言 - REST作为一种现代网络应用非常流行的软件架构风格,自从Roy Fielding博士在2000年他的博士论文中提出来到现在已经有了20年的历史.它的简单易用性,可扩展性,伸 ...
- 干掉RESTful:GraphQL真香!
点击上方"朱小厮的博客",选择"设为星标" 后台回复"书",获取近百本电子书入口 封面:重庆洪崖洞 来源:http://suo.im/5W1 ...
最新文章
- DAL(数据库访问层)
- metasploit快速入门(一)安装部署
- android singleInstance返回问题
- C++11 中STL库中新增内容
- Spring 4.1和Java 8:java.util.Optional
- 【Python CheckiO 题解】I Love Python!
- 职场中什么样的员工最易发展?
- 计算机组成原理与应用,计算机组成原理简答与应用
- rust-vmm 学习
- 数据库之间的远程操作
- FreeImage的学习资料汇总
- matlab 语法_MATLAB中的语法
- 从开发平台到智能供应链,AI技术如何推动企业智能化升级?
- RGB颜色规律小研究
- Python爬虫新手教程:微医挂号网医生数据抓取!
- 计算机基础知识离线作业答案,2013浙大离线作业1.计算机基础知识题
- 如何查看历史版本的Go文档?嘘!答案我只告诉你!
- 在 Ubuntu 上安装 jstest-gtk 手柄测试
- Spotify Mac版(音乐播放器)
- Excel自动为有值的单元格添加边框
热门文章
- 如何用短代码实现内容WordPress会员登录可见?
- 计算机键盘可为哪几个区,电脑键盘分为哪几种?
- Fedora 17: 安装 perl-Tk
- (CRON) info (No MTA installed, discarding output)” error in the syslog
- VMware 安装Ubuntu22.04
- 计算机开机键鼠无法识别,外设门诊:鼠标开机不认 需重插拨怎么办
- win10双显示屏,分屏显示内容
- CPU之外,国产芯片再突破,再也不用看AMD和NVIDIA的脸色了
- 【一步一步学习VBA】Excel VBA 读取txt文件并写入Excel单元格(友好版)
- 选择题库中的试题动态生成试卷(word格式)