记一次GraphQL真正的详细入门(原生,koa2,vue中的实战, 建议收藏)分享会

记录了组内技术分享会, 有一样需求的同窗能够参考一下
分享全程下来时间大约 55分钟

前言

痛点:网上找的资料,文章, GraphQL的官网,一看就是很‘自我’的大神写的(太烂了)彻底管读者能不能看懂,举例子只讲用法!不告诉代码怎么实现的(可是当你学完这一篇你就能够看懂了), 而且历来不晒出总体代码,致使根本不知道他们怎么玩的,有被冒犯到!!能够说那些资料都不适合入门。
定位:GraphQL并非必须用的技术, 但它是必须会的技术,之因此说它必会是由于能够靠它为‘前端’这一行业占领更大的‘领地’, 同时它的思想是值得琢磨与体会的。
是啥:他不是json更不是js, 他是GraphQL自身的语法, 兼容性很是好.
选择:GraphQL为了使开发更高效、简洁、规范而生,若是对工程对团队形成了负担能够果断舍弃(别犹豫,这小子不是必需品),毕竟服务端多一层处理是会消耗性能的,那么就要理智计算他的“损失收益比”了。
前提:学习这门技术须要有必定的“先后交互”的知识储备, 好比会node, 这里会介绍如何在node端使用, 如何集成入koa2项目里面使用, 而且会舍弃一些你们都用的技术, 不作跟风党。

正文

一. GraphQL到底干啥的?用它有啥好处哦?

这里是关键, 必定要引发你们的兴趣, 否则很难进行。

①: 严格要求返回值
好比下面是后端返回的代码前端

{
  name:1,
  name:2,
  name:3,
 }

前端想要的代码vue

{
 name:1
}

从上面能够看出, name2 与 name3 其实我不想要, 那你传给我那么多数据干什么,单纯为了浪费带宽吗?可是吧。。也可理解为某些场景下确实很鸡肋,可是请求多了效果就厉害了。node

②: 设想一个场景, 我想要经过文章的id获取做者信息, 再经过做者信息里面的做者id请求他其余的做品列表, 那么我就须要两步才能完成, 可是若是这两个请求在服务端完成那么咱们只用写一个请求, 并且还不用找后端同窗写新接口。ios

③: 控制默认值: 好比一个做品列表的数组, 没有做品的时候后端很肯能给你返的是null, 可是咱们更想保证一致性但愿返回[],这个时候能够用GraphQL进行规避.git

二. 原生GraphQL尝鲜。

随便建个空白项目npm install graphql.
入口路径以下src->index.jsgithub

var { graphql, buildSchema } = require('graphql');

// 1: 定义模板/映射,  有用mongoose操做数据库经验的同窗应该很好理解这里
var schema = buildSchema(`
  type Query {
    # 我是备注, 这里的备注都是单个井号;
    hello: String
    name: String
  }
`);

// 2: 数据源,能够是热热乎乎从mongodb里面取出来的数据
var root = { 
  hello: () => 'Hello!',
  name:'金毛cc',
  age:5
};

// 3: 描述语句, 我要取什么样的数据, 我想要hello与name 两个字段的数据, 其余的不要给我
const query =  '{ hello, name }'

// 4: 把准备好的食材放入锅内, 模型->描述->整体的返回值
graphql(schema, query, root).then((response) => {
  console.log(JSON.stringify(response));
});

上面的代码直接 node就能够,结果以下: {"data":{"hello":"Hello!","name":"金毛cc"}};web

逐一攻克
1: buildSchema 创建数据模型mongodb

var schema = buildSchema(
// 1. type: 指定类型的关键字
// 2. Query: 你能够理解为返回值的固定类型
// 3. 他并非json,他是graphql的语法, 必定要注意它没有用','
// 4. 返回两个值, 而且值为字符串类型, 注意: string小写会报错
` type Query {
    hello: String
    name: String
  }
`);

GraphQL 中内置有一些标量类型 String、 Int、 Float、 Boolean、 ID,这几种都叫scalar类型, 意思是单个类型vuex

2: const query = '{ hello, name }'
作外层{}基本不变, hello的意思就是我要这一层的hello字段, 注意这里用','分割, 以后会把这个','优化掉.数据库

到这里一个最基本的例子写出来了, 感受也没啥是吧, 我当时学到这里也感受会很顺利, 可是... 接下来文章断层, 官网表达不清, 社区不完善等等问题克服起来好心酸.

三. 利用库更好的原生开发

毕竟这样每次node命令执行不方便, 而且结果出如今控制台里也很差看, 因此咱们要用一个专业工具'yoga'.

yarn add graphql-yoga

const { GraphQLServer } =  require('graphql-yoga');

// 类型定义 增删改查
const typeDefs = `
  type Query{
    hello: String! #必定返回字符串
    name: String
    id:ID!
  }
`
const resolvers = {
  Query:{
    hello(){
      return '我是cc的主人'
    },
    name(){
      return '鲁鲁'
    },
    id(){
      return 9
    },
  }
}
const server = new GraphQLServer({
  typeDefs,
  resolvers
})

server.start(()=>{
  console.log('启动成功, 默认是4000')
})

固然最好用nodemon来启动文件 npm install nodemon -g

  1. hello: String! 像这种加了个'!'就是必定有值的意思, 没值会报错.
  2. Query 这里定义的返回值, 对应函数的返回值会被执行.
  3. new GraphQLServer 的传参 定义的数据模型, 返回值, 由于具体的请求语句须要咱们在web上面输入.
  4. id的类型使用ID这个会把id转换为字符串,这样设计也许是为了兼容全部形式的id.
  5. server.start 很贴心的起一个服务配置好后效果以下:左边是输入, 右边是返回的结果

jm.png

四. 多层对象定义

咱们返回data给前端,基本都会有多层, 那么定义多层就要有讲究了

const {GraphQLServer} =  require('graphql-yoga');
const typeDefs = `
  type Query{
     me: User!  # 这里把me这个key对应的value定义为User类型, 而且必须有值
  }
  type User { # 首字母必须大写
    name:String
  }
`
const resolvers = {
  Query:{
    me(){
      return {
        id:9,
        name:'lulu'
      }
    }
    
  }
}
const server = new GraphQLServer({
  typeDefs,
  resolvers
})

server.start(()=>{
  console.log('启动成功, 默认是4000')
})
  1. User类型不是原生自带 , 因此咱们要本身用type关键字定义一个User数据类型.(首字母必须大写)
  2. Query里面return的值, 必须知足User类型的定义规则

当咱们取name的值时:
id.png

我刚才故意在返回值里面写了id, 那么能够取到值么?

did.png

结论: 就算数据里面有, 可是类型上没有定义, 那么这个值就是取不到的.

五. 数组

定义起来会有些特殊

const { GraphQLServer } = require('graphql-yoga');

const typeDefs = `
  type Query {
    # 返回是数组
    arr:[Int!]! 
  }
`
const resolvers = {
  Query: {
    arr() {
      return [1, 2, 3, 4]
    }
  }
}

const server = new GraphQLServer({
  typeDefs,
  resolvers
})

server.start(() => {
  console.log('启动成功, 默认是4000')
})
  1. arr:[Int!] 若是写成 arr:[] 会报错, 也就是说必须把数组里面的类型定义彻底.
  2. Query里面的返回值必须严格按照type里面定义的返回, 否则会报错.

结果以下:
arr.png

六. 传参(前端能够传参数,供给服务端函数的执行)这个思路很神奇吧.

const { GraphQLServer } = require('graphql-yoga');

const typeDefs = `
  type Query{
     greeting(name: String):String # 须要传参的地方, 必须在这里定义好
     me: User!
  }
  type User { # 必须大写
    name:String
  }
`
const resolvers = {
  Query: {
    // 四个参数大有文章
    greeting(parent, args, ctx, info) {
      return '默认值' + args.name
    },
    me() {
      return {
        id: 9,
        name: 'lulu'
      }
    }

  }
}

const server = new GraphQLServer({
  typeDefs,
  resolvers
})

server.start(() => {
  console.log('启动成功, 默认是4000')
})
  1. greeting(name: String):String greeting是key没的说, 他接收一个name参数为字符串类型, 这里必须指明参数名字, 返回值也必须是字符串类型, 也就是greeting是一个字符串.
  2. greeting(parent, args, ctx, info) { 这里咱们用到 args也就是参数的集合是个对象, 咱们args.name就能够取到name的值, 剩下的值后面用到会讲.
  3. 既然说了要传参, 那就必须传参否则会报错

changcan.png

由于左侧的参数是要放在url请求上的, 因此要用双引号;

七. 关联关系

就像数据库建表同样, 咱们不可能把全部数据放在一张表里, 咱们可能会用一个id来指定另外一张表里面的某些值的集合.
const { GraphQLServer } = require('graphql-yoga');

const typeDefs = `
  type Query{
    lulu: User!
  }
  type User{
    name:String
    age: Int
    chongwu: Chongwu!
  }
  type Chongwu{
     name:String!
     age:Int
  }
`
// 自定义的数据
const chongwuArr = {
  1: {
    name: 'cc',
    age:8
  },
  2: {
    name: '芒果',
    age:6
  },
  9: {
    name: '芒果主人',
    age:24
  }
}

const resolvers = {
  Query: {
    lulu() {
      return {
        name: '鲁路修',
        age: 24,
        chongwu: 9
      }
    },
  },
  // 注意, 它是与Query并列的
  User:{
    // 1: parent指的就是 user, 经过他来获得具体的参数
    chongwu(parent,args,ctx,info){
        console.log('=======', parent.chongwu ) // 9
        return chongwuArr[parent.chongwu]
    }
  }
}

const server = new GraphQLServer({
  typeDefs,
  resolvers
})

server.start(() => {
  console.log('启动成功, 默认是4000')
})

这里数据量有点多, 我慢慢解析

  1. lulu属于User类, User类里面的chongwu(宠物)属于Chongwu类, 咱们须要根据chongwu输入的id 查询出 展现哪一个宠物.
  2. 因为这个宠物的列表可能来自外部, 因此他的定义方式须要与Query同级.
  3. parent 指的就是父级数据, 也就是经过他能够获取到输入的id.

效果以下:
ddai.png

这里就能够解释刚开始的一个问题, 就是那个经过文章id找到做者, 经过做者找到其余文章的问题, 这里的知识点就可让咱们把两个接口合二为一, 或者合n为一.

八. 不是获取, 是操做.

有没有发现上面我演示的都是获取数据, 接下来咱们来讲说操做数据, 也就是'增删改'没有'查'
graphql规定此类操做须要放在Mutation这个类里面, 相似vuex会要求咱们按照他的规范进行书写
const { GraphQLServer } = require('graphql-yoga');

const typeDefs = `
  type Query{
    hello: String!
  }
  # 是操做而不是获取, 增删改:系列
  type Mutation{
    createUser(name:String!, age:Int!):CreateUser

    # 这里面能够继续书写create函数...
  }

  type CreateUser{
    id:Int
    msg:String
  }
`

const resolvers = {
  Query: {
    hello() {
      return '我是cc的主人'
    },
  },
  // query并列
  Mutation: {
    createUser(parent, args, ctx, info) {
      const {name,age} = args;
      // 这里咱们拿到了参数, 那么就能够去awit 建立用户
       return {
         msg:'建立成功',
         id:999
       }
    }
  }
}



const server = new GraphQLServer({
  typeDefs,
  resolvers
})

server.start(() => {
  console.log('启动成功, 默认是4000')
})
  1. Mutation是特殊类, 也是与Query并列.
  2. 一个Mutation里面能够写多个函数, 由于他是个集合.
  3. 为函数的返回值也能够定义类型

效果以下: 接收id与提示信息
creat.png

九. input特殊类型

const { GraphQLServer } = require('graphql-yoga');

const typeDefs = `
  type Query{
    hello: String!
  }
  # 是操做而不是获取, 增删改:系列
  type Mutation{
    # 这个data随便叫的, 叫啥都行, 就是单独搞了个obj包裹起来而已, 不咋地 
    createUser(data: CreateUserInput):CreateUser
  }

  type CreateUser{
    id:Int
    msg:String
  }
   
  # input 定义参数
  input CreateUserInput{
    # 里面的类型只能是基本类型
    name: String!
    age:Int!
  }
`

const resolvers = {
  Query: {
    hello() {
      return '我是cc的主人'
    },
  },
  // query并列
  Mutation: {
    createUser(parent, args, ctx, info) {
      // **这里注意了, 这里就是data了, 而不是分撒开的了**
      const { data } = args;
      return {
        msg: '建立成功',
        id: 999
      }
    }
  }
}

const server = new GraphQLServer({
  typeDefs,
  resolvers
})

server.start(() => {
  console.log('启动成功, 默认是4000')
})
  1. 把参数放在data里面, 而后定义data的类
  2. 注意三个关键点的代码都要改

效果与上面的没区别, 只是多包了层data如图:
data.png

这个只能说有利有弊吧, 多包了一层, 还多搞出一个类, 看似能够封装了实则'鸡肋啊鸡肋'

十. '更鸡肋的'MutationType特殊类型

const {GraphQLServer} =  require('graphql-yoga');

// 很是鸡肋, 这种事作不作, 该不应你作, 内心没点数
const typeDefs = `
  type Query{
    hello: MutationType
  }

  enum MutationType{
    aaaa
    bbbb
    cccc
  }
`
const resolvers = {
  Query:{
    hello(){
      // 只能返回菜单里面的内容, 这样能够保证不出格... p用
      return 'bbbb'
    },
  }
}

const server = new GraphQLServer({
  typeDefs,
  resolvers
})

server.start(()=>{
  console.log('启动成功, 默认是4000')
})
  1. 我定义了一个MutationType的类, 限制只能用'aaa','bbb','ccc'中的一个字符串.
  2. 这不猫捉耗子么? graphql自己定位不是干这个事的, 这种事情交给统一的数据校验模块完成, 他作了校验的话那么其余状况他管无论? 关了又如何你又不改数据, 就会个报错公鸡想下蛋.
  3. 彻底不建议用这个, 当作了解, 具体的校验模块本身在中间件或者utils里面写.

十一. 集成进koa2项目

1. 终于到实战了, 讲了那么多的原生就是为了从最基本的技术点来理解这里
2. 并不必定彻底使用graphql的规范, 彻底能够只有3个接口用它
3. 咱们刚才写的那些type都是在模板字符串里面, 因此确定有人要他模板拆解开, 以对象的形式去书写才符合人类的习惯.

先创建一个koa的工程
// 若果你没有koa的话, 建议你先去学koa, koa知识点比较少因此我暂时没写相应的文章.
koa2 graphqlx // main:工程名 不要与库重名
npm install graphql koa-graphql koa-mount --save 大朗快把库安装好.

app.js文件里面
const Koa = require('koa')
const app = new Koa()
const views = require('koa-views')
const json = require('koa-json')
const onerror = require('koa-onerror')
const bodyparser = require('koa-bodyparser')
const logger = require('koa-logger')

////// 看这里
const mount = require('koa-mount');
const graphqlHTTP = require('koa-graphql');
const GraphQLSchema=require('./schema/default.js');
//////

const index = require('./routes/index')
const users = require('./routes/users')

// error handler
onerror(app)

// middlewares
app.use(bodyparser({
  enableTypes:['json', 'form', 'text']
}))
app.use(json())
app.use(logger())
app.use(require('koa-static')(__dirname + '/public'))

app.use(views(__dirname + '/views', {
  extension: 'pug'
}))

// 每个路径, 对应一个操做
app.use(mount('/graphql', graphqlHTTP({
  schema: GraphQLSchema,
  graphiql: true // 这里能够关闭调试模式, 默认是false
})));

// logger
app.use(async (ctx, next) => {
  const start = new Date()
  await next()
  const ms = new Date() - start
  console.log(`${ctx.method} ${ctx.url} - ${ms}ms`)
})

// routes
app.use(index.routes(), index.allowedMethods())
app.use(users.routes(), users.allowedMethods())

// error-handling
app.on('error', (err, ctx) => {
  console.error('server error', err, ctx)
});

module.exports = app
  1. 我直接吧默认配置也粘进来了, 这样能够保证你拿走就用
  2. graphiql: true 这个时候开启了调试模式 会出现下图的调试界面, 默认是false
  3. mount 来包裹总体的路由
  4. graphqlHTTP 定义请求相关数据
  5. GraphQLSchema 使咱们接下来要写的一个操做模块.

scxs.png

这个画面是否是似曾相识!

schema->default.js
const {
  GraphQLID,
  GraphQLInt,
  GraphQLList,
  GraphQLString,
  GraphQLSchema,
  GraphQLNonNull,
  GraphQLObjectType,
  GraphQLInputObjectType,
} = require('graphql');

// id对应的详情
let idArr = {
  1:{
    name:'我是id1',
    age:'19'
  },
  2:{
    name:'我是id2',
    age:'24'
  }
} 

// 定义id的类
let GID= new GraphQLObjectType({
  name: 'gid',
  fields: {
    name: { type: GraphQLString },
    age: { type: GraphQLString },
  }
})

// 参数类型 不太对
let cs = new GraphQLInputObjectType({
  name:'iddecanshu',
  fields: {
    id: { type: GraphQLString },
  }
})

//定义导航Schema类型
var GraphQLNav = new GraphQLObjectType({
  name: 'nav',
  fields: {
    cc:{ // 传参
      type:GraphQLString,
      // args:new GraphQLNonNull(cs), // 1; 这种是错的
      args:{
        data: {
          type:new GraphQLNonNull(cs), // 这种能够用data为载体了
        } 
      },
      // args:{ // 3:这种最好用了。。。
      //   id:{
      //     type:GraphQLString
      //   }
      // },
      resolve(parent,args){
        return '我传的是' + args.data.id
      }
    },
    // greeting(name: String):String 
    title: { type: GraphQLString },
    url: { type: GraphQLString },
    id: {
      // type:GraphQLList(GID), // 这里很容易忽略
      type:GraphQLNonNull(GID), // 反复查找也没有专门obj的 这里用非空代替
      async resolve(parent,args){
        // console.log('wwwwwwwww', idArr[parent.id])
        // 这个bug我tm。。。。。
        // 须要是数组形式。。。。否则报错
        // "Expected Iterable, but did not find one for field \"nav.id\".",
        // return [idArr[parent.id]];

        // 2: 更改类型后就对了
        return idArr[parent.id] || {}
      }
    },
  }
})

//定义根
var QueryRoot = new GraphQLObjectType({
  name: "RootQueryType",
  fields: {
    navList: {
      type: GraphQLList(GraphQLNav),
      async resolve(parent, args) {
        var navList = [
          { title: 'title1', url: 'url1', id:'1' },
          { title: 'title2', url: 'url2', id:'2' }
        ]
        return navList;
      }
    }
  }
})

//增长数据
const MutationRoot = new GraphQLObjectType({
  name: "Mutation",
  fields: {
    addNav: {
      type: GraphQLNav,
      args: {
        title: { type: new GraphQLNonNull(GraphQLString) },
      },
      async resolve(parent, args) {
        return {
          msg: '插入成功'
        }
      }
    }
  }
})

module.exports = new GraphQLSchema({
  query: QueryRoot,
  mutation: MutationRoot
});

十二. koa2中的使用原理"逐句"解析

①引入
  1. 这个是原生自带的, 好比咱们会把GraphQLID 这种象征着单一类型的类单独拿到.
  2. 定义type的方法也变成了 GraphQLObjectType这样的实例化类来定义.
const {
  GraphQLID,
  GraphQLInt,
  GraphQLList,
  GraphQLString,
  GraphQLSchema,
  GraphQLNonNull,
  GraphQLObjectType,
  GraphQLInputObjectType,
} = require('graphql');
②单一的类
  1. 咱们实例化GraphQLObjectType导出一个'type'
  2. 使用 type:GraphQLString的形式规定一个变量的类型
  3. name: 这里的name能够理解为一个说明, 有时候能够经过获取这个值作一些事.
let GID= new GraphQLObjectType({
  name: 'gid',
  fields: {
    name: { type: GraphQLString },
    age: { type: GraphQLString },
  }
})
③ 定义根类
  1. fields必需要写, 在它里面才能够定义参数
  2. GraphQLList意思就是必须为数组
  3. type不能少, 里面要规定好这组返回数据的具体类型
  4. resolve也是必须有的没有会报错, 而且必须返回值与type一致
var QueryRoot = new GraphQLObjectType({
  name: "RootQueryType",
  fields: {
    navList: {
      type: GraphQLList(GraphQLNav),
      async resolve(parent, args) {
        var navList = [
          { title: 'title1', url: 'url1', id:'1' },
          { title: 'title2', url: 'url2', id:'2' }
        ]
        return navList;
      }
    }
  }
})

十三. koa2里面的关联关系与传参

这里的关联关系是指, 以前咱们说过的 id 指向另外一个表
let GID= new GraphQLObjectType({
  name: 'gid',
  fields: {
    name: { type: GraphQLString },
    age: { type: GraphQLString },
  }
})
var GraphQLNav = new GraphQLObjectType({
  name: 'nav',
  fields: {
    cc:{
      type:GraphQLString,
      args:{
        data: 
          type:new GraphQLNonNull(cs), // 这种能够用data为载体了
        } 
      },
      resolve(parent,args){
        return '我传的是' + args.data.id
      }
    },
    id: {
      type:GraphQLNonNull(GID),
      async resolve(parent,args){
        return idArr[parent.id] || {}
      }
    },
  }
})
  1. 上面cc这个变量比较特殊, 他须要args这个key来规范参数, 这里能够直接写参也能够像这里同样抽象一个类.
  2. id他规范了id对应的是一个对象, 里面有name有age
  3. cc想要拿到传参就须要args.data 由于这里咱们用的input类来作的

实际效果如图所示:

ccnz.png

十四. 对集成在koa2内的工程化思考(数据模型分块)

1. 从上面那些例子里面可看出, 咱们能够用/api/blog这种路由path为单位, 去封装一个个的数据模型
2. 每一个模型里面其实都须要操做数据库
3. 说实话增长的代码有点多, 这里只演示了2个接口就已经这么大了
4. 学习成本是不可忽略的, 并且这里面各类古怪的语法报错

十五. 前端的调用

这里咱们以vue为例
import axios from "axios"; 这个是前提
query= 这个是关键点, 咱们之后的参数都要走这里

方式1(暴力调取)
created(){
    // 1: 查询列表
    // ①: 必定要转码, 由于url上不要有{} 空格
    axios
      .get(
        "/graphql?query=%7B%0A%20%20navList%20%7B%0A%20%20%20%20title%0A%20%20%20%20url%0A%20%20%7D%0A%7D%0A"
      )
      .then(res => {
        console.log("返回值: 1", res.data);
      });
     }
方式2(封装函数)
methods: {
    getQuery() {
      const res = `
        {
          navList {
            title
            url
            id {
              name
              age
            }
          }
        }`;
      return encodeURI(res);
    },
  },
  created() {
    axios.get(`/graphql?query=${this.getQuery()}`).then(res => {
      console.log("返回值: 2", res.data);
    });
  }
方式3(函数传参)
methods: {
    getQuery2(id) {
      const res = `
        {
          navList {
            cc(data:{id:"${id}"})
            title
            url
            id {
              name
              age
            }
          }
        }`;
      return encodeURI(res);
    }
  },
  created() {
    axios.get(`/graphql?query=${this.getQuery2(1)}`).then(res => {
      console.log("返回值: 3", res.data);
    });
  }

十六. 前端插件的调研

  1. 一看前面的传参方式就会发觉, 这确定不合理啊, 必定要把字符串解构出来.
  2. vue-apollo技术栈是当前比较主流的, 可是这个库写的太碎了, 而且配置起来还要更改我原本的代码习惯.
  3. 又在github上面找了一些模板的库, 可是并无让我从书写字符串的尴尬中解脱出来

因此暂时没有找到我承认的库, 固然了本身暂时也并不想把时间放在开发这个插件上.

十七. 我想要的插件什么样的

  1. 可使我,采用对象或者json的方式来定义query
  2. 不须要定义那么多概念, 开箱即用, 我只须要那个核心的模板解决方案
  3. 不要改变我原本的代码书写方式, 好比说vue-apollo提供了新的请求方式, 但是个人项目都是axios, 我就是不想换 凭什么要换

十八. 学习graphql有多少阻碍

  1. 网上的资料真的对初学者很不友好, 根本不举例子, 致使每一个知识点都会我本身试了几十次试出来的.
  2. 光学这一个技术不够, 还要思考服务端的重构与新规范, 前端也要新规范.
  3. 这个技术也出来好几年了, 可是社区真的不敢恭维, 因此啊仍是须要本身更多的精力投入进来.

十九. 此类技术趋势的思考

  1. 前端工程师愈来愈不知足本身的代码只在浏览器上运行了, 咱们也要参与到服务端交互的环节中.
  2. 身为当今2020年的前端若是眼光还局限在web端有可能会被后浪排在沙滩上了.
  3. 我的不太喜欢这种抛出不少新的知识点, 和架构体系的库, 应该是越少的学习成本与代码成本作到最多的事.
  4. 前端技术还在摸索阶段, 也能够说是扩展阶段, 预计这几年仍是会出现各类新的概念, 可是吧正所谓"合久必分,分久必合", 在不就的未来前端的范围极可能从新划定.

二十. end

说了这么多也是由于学习期间遇到了好多坑, 无论怎么样最后仍是可使用起来了, 在以后的使用过程当中仍是会不断的总结并记录, 遇到问题咱们能够一块儿讨论.但愿和你一块儿进步.

相关文章
相关标签/搜索