Skip to content

Latest commit

 

History

History
1038 lines (793 loc) · 21 KB

24.云笔记开发实录.md

File metadata and controls

1038 lines (793 loc) · 21 KB

云笔记 开发实录

1、通过脚手架创建项目

进入项目文件夹

  1. 全局安装 Vue-cli
npm install -g vue-cli
  1. 使用 Vue-cli 创建 基于webpack模板的新项目
 vue init webpack vue-cloud-note
  1. 选择如下

    image-20220409231828241

image-20220409231944142等待漫长的安装。。。。

2、进入项目

  1. 进入项目

    cd vue-cloud-note
    
  2. 安装依赖

    npm install
    
  3. 启动项目

    npm run dev
    
  4. 显示以下画面说明大功告成

    image-20220409234131521

3、Vue-router 初体验

  1. src/components下创建Logon.vue文件,并输入以下代码

    <template>
      <div id="login">
        <h1>{{ msg }}</h1>
      </div>
    </template>
    
    <script>
    export default {
      name: 'Login',
      data() {
        return {msg: '这是登录页面'}
      }
    }
    </script>
    
    <style scoped>
    h1{ color: #f60; }
    </style>
  2. router/index.js文件中,添加如下代码

      import Vue from 'vue'
      import Router from 'vue-router'
      import HelloWorld from '../components/HelloWorld'
    + import Login from '../components/Login'
    
      Vue.use(Router)
    
      export default new Router({
        routes: [
          {
            path: '/',
            name: 'HelloWorld',
            component: HelloWorld
          },
    +      {
    +        path: '/login',
    +        name: 'Login',
    +        component: Login
    +      }
        ]
      })

4、继续完善页面路由

经过上面的例子,我相信你已经大概知道路由是怎么用的,因此我们还需要在添加几个页面

  1. src/components中依次添加NotebookList.vueNoteDetail.vueTashDetail.vue文件

image-20220410002031454

  1. router/index.js中添加路由

      import Vue from 'vue'
      import Router from 'vue-router'
      import Login from '../components/Login'
    - import HelloWorld from '../components/HelloWorld'
    + import NotebookList from '../components/NotebookList.vue'
    + import NoteDetail from '../components/NoteDetail.vue'
    + import TashDetail from '../components/TashDetail.vue'
      
      Vue.use(Router)
      
      export default new Router({
        routes: [
          {
            path: '/login',
            name: 'Login',
            component: Login
     +    }, {
     +      path: '/notebooks',
     +      name: 'NotebookList',
     +      component: NotebookList
     +    }, {
     +      path: '/note/:noteId',
     +      name: 'NoteDetail',
     +      component: NoteDetail
     +    }, {
     +      path: '/trash/:noteId',
     +      name: 'TashDetail',
     +      component: TashDetail
     +    }
     +  ]
     +})

    新增的三个界面代码可点击此链接查看

    在页面输入不同的路由测试无误,说明路由配置都正确啦

5、新增 Slidebar 组件

代码记录

6、新增 Avatar 组件

代码记录

7、安装 less

npm install --save-dev less-loader@4

Sidebar.vuestyle 改写成 less语法

代码提交记录

8、修改登录组件模板和样式

代码提交记录

9、登录注册信息校验完成

代码提交记录

挑重点记录一下

...
<div class="button" @click="onRegister">创建账号</div>
...
<div class="button" @click="onLogin">登录</div>
...
<script>
    
export default{
//点击注册按钮触发事件
onRegister() {
  let result1 = this.validUsername(this.register.username)
  if (!result1.isValid) {
    this.register.isError = true
    this.register.notice = result1.notice
    return
  }
  let result2 = this.validPassword(this.register.password)
  if (!result2.isValid) {
    this.register.isError = true
    this.register.notice = result2.notice
    return
  }
  this.register.isError = false
  this.register.notice = ''
  console.log('注册',
    '用户名是:', this.register.username,
    '密码是:', this.register.password
  )
},
//点击登录按钮触发事件
onLogin() {
  let result1 = this.validUsername(this.login.username)
  if (!result1.isValid) {
    this.login.isError = true
    this.login.notice = result1.notice
    return
  }
  let result2 = this.validPassword(this.login.password)
  if (!result2.isValid) {
    this.login.isError = true
    this.login.notice = result2.notice
    return
  }
  this.login.isError = false
  this.login.notice = ''
  console.log('登录',
    '用户名是:', this.login.username,
    '密码是:', this.login.password
  )
},

//设计校验用户名、密码函数
validUsername(username) {
  return {
    isValid: /^[a-zA-Z_0-9]{3,15}$/.test(username),
    notice: '用户名必须是3-15位数,且仅限字母、数字、下划线'
  }
},
validPassword(password) {
  return {
    isValid: /^.{6,16}$/.test(password),
    notice: '密码长度在6-16位以内'
  }
}
}
</script>

10、接口封装 !!!

component目录下新建helper\request.js

安装axios

npm install --save axios

requset.js引入axios

定义请求头和根路径

axios.defaults.headers.post['Content-Type'] = 'application/x-www-from-urlencoded'
axios.defaults.baseURL = 'https://note-server.hunger-valley.com/'

export default function request(url, type = 'GET', date = {}) {
  return new Promise((resolve, reject) => {
      let option = {
        url,
        method: type,
        validateStatus(status) {
          return (status >= 200 && status < 300) || status === 400
        },
      }
      if (type.toLowerCase() === 'get') {
        option.params = date
      } else {
        option.data = date
      }
      axios(option).then(res => {
        if (res.status === 200) {
          resolve(res.data)
        } else {
          console.log('11111')
          console.log(res.data)
          reject(res.data)
        }
      }).catch(err => {
        console.log({msg: '网络异常'})
        reject({msg: '网络异常'})
      })
    }
  )
}

由于当前访问域名是http://localhost:...

而后端接口提供的域名是https://note-server.hunger-valley.com/

存在跨域,此时由于后端接口已经允许跨域请求

image-20220410232559137

因此前端需要在request.js再添加允许跨域,才能在登录请求的时候带上cookies

axios.defaults.withCredentials = true

11、封装接口成API

新建src/apis/auth.js

import request from '../helpers/request'

const URL = {
  REGISTER: '/auth/register',
  LOGIN: '/auth/login',
  LOGOUT: '/auth/logout',
  GET_INFO: '/auth'
}

export default {
  register({username, password}) {
    return request(URL.REGISTER, 'POST', {username, password})
  },
  login({username, password}) {
    return request(URL.LOGIN, 'POST', {username, password})
  },
  logout() {
    return request(URL.LOGOUT)
  },
  getInfo() {
    return request(URL.GET_INFO)
  }
}

当需要调用接口时

Api.login({
    username: this.login.username,
	password: this.login.password
}).then(data => {
	console.log(data)
})

12、生产环境和开发环境 baseURL 无缝切换

在实际开发中,通常后端接口时没有写好的,这时候就需要用一些测试的url来进行对接,因此测试环境和生产环境的url不一致,经常需要手动切换很不方便。

新建build/mock.config.js

const fs = require('fs')
const path = require('path')

const mockBaseURL = 'http://localhost:3000'
const realBaseURL = 'https://note-server.hunger-valley.com/'

exports.config = function ({isDev = true} = {isDev: true}) {
  let fileTxt = `
    module.exports={
      baseURL:'${isDev ? mockBaseURL : realBaseURL}'
    }`
  fs.writeFileSync(path.join(__dirname, '../src/helpers/config-baseURL.js'), fileTxt)
}

如何使用:

build/webpack.dev.conf.js添加

require('./mock.config').config({isDev: true})

build/webpack.prod.conf.js添加

require('./mock.config').config({isDev: false})

helper/request.js中引用

import baseURLConfig from './config-baseURL'

axios.defaults.baseURL = baseURLConfig

13、登录、注册、注销后页面跳转

Api.register({
  username: this.register.username,
  password: this.register.password
}).then(data => {
  this.register.isError = false
  this.register.notice = ''
  this.$router.push({path: '/notebooks'})
}).catch(data => {
  this.register.isError = true
  this.register.notice = data.msg
})
Api.login({
	username: this.login.username,
	password: this.login.password
}).then(data => {
	this.login.isError = false
	this.login.notice = ''
	this.$router.push({path: '/notebooks'})
}).catch(data => {
	console.log(data)
	this.login.isError = true
	this.login.notice = data.msg
})

14、Avatar 组件展示用户昵称显示

新建src/helper/bus.js

import Vue from 'vue'

export default new Vue()

/* 使用方法:
import Bus from '../helper/bus'

绑定事件:
Bus.$on('test',msg=>{
  console.log(msg)
})

触发事件:
Bus.$emit('test','hello')

*/

Login.vue组件,绑定emit事件(onRegister 注册函数同理)

Api.login({
    username: this.login.username,
    password: this.login.password
}).then(data => {
    this.login.isError = false
    this.login.notice = ''
+   Bus.$emit('userInfo', {username: this.login.username})
    this.$router.push({path: '/notebooks'})
}).catch(data => {
    console.log(data)
    this.login.isError = true
    this.login.notice = data.msg
})

Avatar.vue组件,触发on事件

export default {
  data() {
    return {
      username: '未登录',
    }
  },
  created() {
    Bus.$on('userInfo', user => {
      this.username = user.username
    })
    Auth.getInfo()
      .then(res => {
        if (res.isLogin) {
          this.username = res.data.username
        }
      })
  },
  computed: {
    slug() {
      return this.username.charAt(0)
    }
  }
};
</script>

15、未登录访问登录页以外的页面都跳转到登录页

componentsNotebookList.vueNoteDetail.vueTashDetail.vue中,添加登录验证,未登录跳转至登录界面

<script>
import Auth from '../apis/auth'

export default {
  name: 'Login',
  data() {
    return {
      msg: '笔记本列表'
    }
  },
  created() {
+   Auth.getInfo()
+     .then(res => {
+       if (!res.isLogin) {
+         this.$router.push({path: '/login'})
+       }
+     })
+ }
}
</script>

16、渲染文章列表数据

src/components/NotebookList.vue

<template>
  <div class="detail" id="notebook-list">
    <header>
      <a href="#" class="btn">
        <i class="iconfont icon-plus"/>
        新建笔记本
      </a>
    </header>
    <main>
      <div class="layout">
        <h3>笔记本列表(10)</h3>
        <div class="book-list">
          <router-link to="/note/1" class="notebook">
            <div>
              <span class="iconfont icon-notebook"></span>文章标题
              <span>3</span>
              <span class="action">编辑</span>
              <span class="action">删除</span>
              <span class="date">3天前</span>
            </div>
          </router-link>
        </div>
      </div>
    </main>
  </div>
</template>

<style scoped lang="less">
#notebook-list {
  flex: 1;
  background: #eeedef;

  .btn {
    font-size: 12px;
    color: #666;
    cursor: pointer;
    margin-left: 10px;
  }

  .btn .iconfont {
    font-size: 12px;
  }

  input {
    width: 300px;
    height: 30px;
    line-height: 30px;
    border: 1px solid #ccc;
    border-radius: 3px;
    padding: 3px 5px;
    outline: none;
  }

  header {
    padding: 12px;
    border-bottom: 1px solid #ccc;
  }

  main {
    padding: 30px 40px;
  }

  .layout {
    max-width: 966px;
    margin: 0 auto;
  }


  main h3 {
    font-size: 12px;
    color: #000;
  }

  main .book-list {
    margin-top: 10px;
    font-size: 14px;
    color: #666;
    background-color: #fff;
    border-radius: 4px;
    font-weight: bold;
  }

  main .book-list span {
    font-size: 12px;
    font-weight: bold;
    color: #b3c0c8;
  }

  main .date,
  main .action {
    float: right;
    margin-left: 10px;
  }

  main .iconfont {
    color: #1687ea;
    margin-right: 4px;
  }

  main .notebook {
    display: block;
    cursor: pointer;
  }

  main a.notebook:hover {
    background-color: #f7fafd;
  }

  main a.notebook div {
    border-bottom: 1px solid #ebebeb;
    padding: 12px 14px;
  }

  main .error-msg {
    font-size: 12px;
    color: red;
  }

}
</style>

17、发起AJAX请求获取真实数据

新建src/apis/notebooks.js

import request from '../helpers/request'

const URL = {
  GET: '/notebooks',
  ADD: '/notebooks',
  UPDATE: '/notebooks/:id',
  DELETE: '/notebooks/:id'
}

export default {
  getAll() {
    return new Promise((resolve, reject) => {
      request(URL.GET)
        .then(res => {
          res.data = res.data.sort((notebook1, notebook2) => notebook1.createdAt < notebook2.createdAt)
          res.data.forEach(notebook=>{
            // notebook.friendlyCreatedAt = friendlyDate(notebook.createdAt)
          })
          resolve(res)
        }).catch(err => {
        reject(err)
      })
    })
  },

  updateNotebook(notebookId, { title = '' } = { title: '' }) {
    return request(URL.UPDATE.replace(':id', notebookId), 'PATCH', { title })
  },

  deleteNotebook(notebookId) {
    return request(URL.DELETE.replace(':id', notebookId), 'DELETE')
  },

  addNotebook({ title = ''} = { title: ''}) {
    return request(URL.ADD, 'POST', { title })
  }

}

Notebooks.vue

<template>
  <div class="detail" id="notebook-list">
    <header>
      <a href="#" class="btn" @click="onCreate">
        <i class="iconfont icon-plus"/>
        新建笔记本
      </a>
    </header>
    <main>
      <div class="layout">
        <h3>笔记本列表({{notebooks.length}})</h3>
        <div class="book-list">
          <router-link to="/note/1" v-for="notebook in notebooks" :key="notebook.id" class="notebook">
            <div>
              <span class="iconfont icon-notebook"></span>{{notebook.title}}
              <span>{{notebook.noteCounts}}</span>
              <span class="action" @click="onEdit(notebook)" >编辑</span>
              <span class="action" @click="onDelete(notebook)">删除</span>
              <span class="date">3天前</span>
            </div>
          </router-link>
        </div>
      </div>
    </main>
  </div>
</template>
<script>
import Auth from '../apis/auth'
import Notebooks from '../apis/notebooks'


export default {
  data() {
    return {
      notebooks: [],
      msg: '笔记本列表'
    }
  },
  created() {
    Auth.getInfo()
      .then(res => {
        if (!res.isLogin) {
          this.$router.push({path: '/login'})
        }
      })
+    Notebooks.getAll()
+      .then(res => {
+        console.log(res)
+        this.notebooks = res.data
+      })
  },
  methods: {
    onCreate() {
      console.log('1')
    },
    onEdit(notebook) {
      console.log('2')
    },
    onDelete(notebook) {
      console.log('3')
    }

  }
}
</script>

18、阻止冒泡事件

由于<a>标签包裹了<span>,因此在点击<span>时会产生默认冒泡,需要阻止

<span class="action" @click.stop.prevent="onEdit(notebook)" >编辑</span>
<span class="action" @click.stop.prevent="onDelete(notebook)">删除</span>

19、展示数据

新建时间优化函数src/hrlper/util.js

export function friendlyDate(dateStr) {
  let dateObj = typeof dateStr === 'object' ? dateStr : new Date(dateStr)
  let time = dateObj.getTime()
  let now = Date.now()
  let space = now - time
  let str = ''


  switch (true) {
    case space < 1000 * 60 :
      str = '刚刚'
      break
    case space < 1000 * 60 * 60 :
      str = Math.floor((space) / (1000 * 60)) + '分钟前'
      break
    case space < 1000 * 60 * 60 * 24 :
      str = Math.floor((space) / (1000 * 3600)) + '小时前'
      break
    default:
      str = Math.floor((space) / (1000 * 3600 * 24)) + '天前'
      break
  }
  return str
}

src/component/NotebookList.vue

methods: {
    onCreate() {
      let title = window.prompt('创建笔记本:')
      if (title.trim() === '') {
        alert('标题不能为空!')
        return
      }
      Notebooks.addNotebook({title})
        .then(res => {
          this.notebooks.unshift(res.data)
          res.data.friendlyCreatedAt = friendlyDate(res.data.createdAt)
          alert(res.msg)
        })
    },
    onEdit(notebook) {
      let title = window.prompt('修改标题', notebook.title)
      Notebooks.updateNotebook(notebook.id, {title})
        .then(res => {
          notebook.title = title
          alert(res.msg)
        })
    },
    onDelete(notebook) {
      let isConfirm = window.confirm('你确定要删除吗?')
      if (isConfirm) {
        Notebooks.deleteNotebook(notebook.id)
          .then(res => {
            this.notebooks.splice(this.notebooks.indexOf(notebook), 1)
            alert(res.msg)
          })
      }

    }

  }

src/apis/notebook.js

...
return new Promise((resolve, reject) => {
      request(URL.GET)
        .then(res => {
          res.data = res.data.sort((notebook1, notebook2) => notebook1.createdAt > notebook2.createdAt ? -1 : 1)
          res.data.forEach(notebook=>{
+           notebook.friendlyCreatedAt = friendlyDate(notebook.createdAt)
          })
          resolve(res)
        }).catch(err => {
        reject(err)
      })
    })
...

实际效果

image-20220412182120447

20、使用element-UI 替换 丑陋的自带 UI

  1. 安装element-ui

    npm install element-ui --save
  2. main.js引入element-ui

    import Vue from 'vue'
    + import ElementUI from 'element-ui'
    + import 'element-ui/lib/theme-chalk/index.css';
    import App from './App'
    import router from './router'
    
    + Vue.use(ElementUI);
    Vue.config.productionTip = false
    
    
    /* eslint-disable no-new */
    new Vue({
      el: '#app',
      router,
      components: {App},
      template: '<App/>'
    })

21、优化代码

点此查看代码

22、通过url获取第对应id

<h1>noteId : {{ $route.params.noteId }}</h1>

25、组件之间信息的传递

vuex

不知道你发现没有,项目做到这里,我们经常需要再不同的组件中都发请求,有时候是需要父子组件进行通讯,有时候是兄弟组件进行通讯,层级少的情况下还尚可,一旦层级变多,整个数据之间的流转就会变得非常复杂,所以引入vuex就变得十分必要。

vuex主要做的就是处理数据之间的分发

vuex的安装

npm install vuex@next --save
/或/
yarn add vuex@next --save

配置 vuex,使其工作起来:在src路径下创建store文件夹,然后创建index.js文件,文件内容如下:

import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

const store = new Vuex.Store({
    state: {
        // 定义一个name,以供全局使用
        name: '张三',
        // 定义一个number,以供全局使用
        number: 0,
        // 定义一个list,以供全局使用
        list: [
            { id: 1, name: '111' },
            { id: 2, name: '222' },
            { id: 3, name: '333' },
        ]
    },
});

export default store;

修改main.js:

可以通过store.state 来获取状态对象,并通过 store.commit 方法触发状态变更:

import Vue from 'vue';
import App from './App';
import router from './router';
import store from './store'; // 引入我们前面导出的store对象

Vue.config.productionTip = false;

new Vue({
    el: '#app',
    router,
    store, // 把store对象添加到vue实例上
    components: { App },
    template: '<App/>'
});

最后修改App.vue

<template>
    <div></div>
</template>

<script>
    export default {
        mounted() {
            // 使用this.$store.state.XXX可以直接访问到仓库中的状态
            console.log(this.$store.state.name); //打印出张三
        }
    }
</script>