第三四章 Vue进阶
e
m
i
t
和
emit 和
emit和on
用this.$on来定义一个事件, 并且指定事件的执行对象(函数), 他主要是用来干什么的呢 --事件的定义和消费 使用this.on来定义一个事件,
好处是什么呢 可以把事件的定义和事件的消费分开,实现逻辑的解耦 可以在子组件中直接调用事件非常灵活方便
来看一下this.on 和this.emit的实现原理是什么
可以打断点试一下,通过对源码的分析可以知道on方法在定义的时候可以同时定义 多个事件,也可以为同一个事件绑定多个处理函数, 也知道try catch进行了错误处理 所以抛出异常的时候不会中断执行
directive用法(指令)
比较复杂,来看一个案例
这是一个很常用的场景,在实际的项目开发过程中经常要用到
来看一下源码
<html>
<head>
<title>directive 用法</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<div id="root">
<div v-loading="isLoading">{{data}}</div>
<button @click="update">更新</button>
</div>
<script>
Vue.directive('loading', {
update(el, binding, vnode) {
if (binding.value) {
const div = document.createElement('div')
div.innerText = '加载中...'
div.setAttribute('id', 'loading')
div.style.position = 'absolute'
div.style.left = 0
div.style.top = 0
div.style.width = '100%'
div.style.height = '100%'
div.style.display = 'flex'
div.style.justifyContent = 'center'
div.style.alignItems = 'center'
div.style.color = 'white'
div.style.background = 'rgba(0, 0, 0, .7)'
document.body.append(div)
} else {
const div= document.getElementById('loading')
div && document.body.removeChild(div)
}
}
})
new Vue({
el: '#root',
data() {
return {
isLoading: false,
data: ''
}
},
methods: {
update() {
this.isLoading = true
setTimeout(() => {
this.data = '用户数据'
this.isLoading = false
}, 3000)
}
}
})
</script>
</body>
</html>
老师后面还有一些源码分析,以后再看吧
Vue.component
用途就是定义一个组件,
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<div id="app">
<Test :msg="message"></Test>
</div>
<script>
Vue.component('Test',{
template:'<div>{{msg}} </div>',
props:{
msg:{
type:String,
default:'hahahaha'
}
}
})
new Vue({
el:"#app",
data(){
return {
message:'test component'
}
},
methods: {
},
})
</script>
</body>
</html>
Vue.extend很重要,他就是用来构造一个组件的
Sub是个构造函数,
defination已经变成了一个function,这个function就是VueComponent
这里有很多属性,我们实例化一个组件的时候包含这些属性 初始化过程最关键的一个地方就是Vue.extend
Vue.extend
这个主要作用是什么呢 就是用来生成组件的构造函数
<html>
<head>
<title>Vue.extend 用法</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<div id="root">
<Test :msg="message"></Test>
</div>
<script>
const component = Vue.extend({
template: '<div>{{msg}}</div>',
props: {
msg: {
type: String,
default: 'default message'
}
},
name: 'Test'
})
Vue.component('Test')
new Vue({
el: '#root',
data() {
return {
message: "Test Extend Component"
}
}
})
</script>
</body>
</html>
Vue.extend进阶
<html>
<head>
<title>Vue.extend 用法2</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<style>
#loading-wrapper {
position: fixed;
top: 0;
left: 0;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
background: rgba(0,0,0,.7);
color: #fff;
}
</style>
</head>
<body>
<div id="root">
<button @click="showLoading">显示Loading</button>
</div>
<script>
function Loading(msg) {
const LoadingComponent = Vue.extend({
template: '<div id="loading-wrapper">{{msg}}</div>',
props: {
msg: {
type: String,
default: msg
}
},
name: 'LoadingComponent'
})
const div = document.createElement('div')
div.setAttribute('id', 'loading-wrapper')
document.body.append(div)
new LoadingComponent().$mount('#loading-wrapper')
return () => {
document.body.removeChild(document.getElementById('loading-wrapper'))
}
}
Vue.prototype.$loading = Loading
new Vue({
el: '#root',
methods: {
showLoading() {
const hide = this.$loading('正在加载,请稍等...')
setTimeout(() => {
hide()
}, 2000)
}
}
})
</script>
</body>
</html>
如何给vue实例添加api 与前面的指令有些不一样,指令是修改某个状态加载的,这个是需要主动去触发的,等于是给vue加了个新的api 可以使项目耦合度大大下降
这里有个很妙的地方
Vue.use
<html>
<head>
<title>Vue.use 用法</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<style>
#loading-wrapper {
position: fixed;
top: 0;
left: 0;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
background: rgba(0,0,0,.7);
color: #fff;
}
</style>
</head>
<body>
<div id="root">
<button @click="showLoading">显示Loading</button>
</div>
<script>
const loadingPlugin = {
install: function(vm) {
const LoadingComponent = vm.extend({
template: '<div id="loading-wrapper">{{msg}}</div>',
props: {
msg: {
type: String,
default: 'loading...'
}
}
}, 'LoadingComponent')
function Loading(msg) {
const div = document.createElement('div')
div.setAttribute('id', 'loading-wrapper')
document.body.append(div)
new LoadingComponent({
props: {
msg: {
type: String,
default: msg
}
}
}).$mount('#loading-wrapper')
return () => {
document.body.removeChild(document.getElementById('loading-wrapper'))
}
}
vm.prototype.$loading = Loading
}
}
Vue.use(loadingPlugin)
new Vue({
el: '#root',
methods: {
showLoading() {
const hide = this.$loading('正在加载,请稍等...')
setTimeout(() => {
hide()
}, 2000)
}
}
})
</script>
</body>
</html>
是用来加载vue的插件,我们把上一节vue.extend的用法做成一个插件,
这个和上面的功能一样,这样写的话可以把loadingPlugin还有style放到另外的文件当中
通过模块加载的方法来解耦
provide inject
组件通信我们知道有个props vuex 为什么还需要provide和inject呢
如果有3个组件的话我们是一层一层往下传的,比较笨重,vuex的话学习的成本比较高,这是vue就推出了provide inject 来解决这个问题,让组件的通信过程变得更加简单
<html>
<head>
<title>组件通信 provide 和 inject</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<div id="root">
<Test></Test>
</div>
<script>
function registerPlugin() {
Vue.component('Test', {
template: '<div>{{message}}<Test2 /></div>',
provide() {
return {
elTest: this
}
},
data() {
return {
message: 'message from Test'
}
},
methods: {
change(component) {
this.message = 'message from ' + component
}
}
})
Vue.component('Test2', {
template: '<Test3 />'
})
Vue.component('Test3', {
template: '<button @click="changeMessage">change</button>',
inject: ['elTest'],
methods: {
changeMessage() {
this.elTest.change(this.$options._componentTag)
}
}
})
}
Vue.use(registerPlugin)
new Vue({
el: '#root'
})
</script>
</body>
</html>
filter过滤器
可以对值进行二次处理
<html>
<head>
<title>过滤器 filter</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<div id="root">
{{message | lower}}
</div>
<script>
new Vue({
el: '#root',
filters: {
lower(value) {
return value.toLowerCase()
}
},
data() {
return {
message: 'Hello Vue'
}
}
})
</script>
</body>
</html>
监听器watch
<html>
<head>
<title>监听器 watch</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<div id="root">
<h3>Watch 用法1:常见用法</h3>
<input v-model="message">
<span>{{copyMessage}}</span>
</div>
<div id="root2">
<h3>Watch 用法2:绑定方法</h3>
<input v-model="message">
<span>{{copyMessage}}</span>
</div>
<div id="root3">
<h3>Watch 用法3:deep + handler</h3>
<input v-model="deepMessage.a.b">
<span>{{copyMessage}}</span>
</div>
<div id="root4">
<h3>Watch 用法4:immediate</h3>
<input v-model="message">
<span>{{copyMessage}}</span>
</div>
<div id="root5">
<h3>Watch 用法5:绑定多个 handler</h3>
<input v-model="message">
<span>{{copyMessage}}</span>
</div>
<div id="root6">
<h3>Watch 用法6:监听对象属性</h3>
<input v-model="deepMessage.a.b">
<span>{{copyMessage}}</span>
</div>
<script>
new Vue({
el: '#root',
watch: {
message(value) {
this.copyMessage = value
}
},
data() {
return {
message: 'Hello Vue',
copyMessage: ''
}
}
})
new Vue({
el: '#root2',
watch: {
message: 'handleMessage'
},
data() {
return {
message: 'Hello Vue',
copyMessage: ''
}
},
methods: {
handleMessage(value) {
this.copyMessage = value
}
}
})
new Vue({
el: '#root3',
watch: {
deepMessage: {
handler: 'handleDeepMessage',
deep: true
}
},
data() {
return {
deepMessage: {
a: {
b: 'Deep Message'
}
},
copyMessage: ''
}
},
methods: {
handleDeepMessage(value) {
this.copyMessage = value.a.b
}
}
})
new Vue({
el: '#root4',
watch: {
message: {
handler: 'handleMessage',
immediate: true,
}
},
data() {
return {
message: 'Hello Vue',
copyMessage: ''
}
},
methods: {
handleMessage(value) {
this.copyMessage = value
}
}
}),
new Vue({
el: '#root5',
watch: {
message: [{
handler: 'handleMessage',
},
'handleMessage2',
function(value) {
this.copyMessage = this.copyMessage + '...'
}]
},
data() {
return {
message: 'Hello Vue',
copyMessage: ''
}
},
methods: {
handleMessage(value) {
this.copyMessage = value
},
handleMessage2(value) {
this.copyMessage = this.copyMessage + '*'
}
}
})
new Vue({
el: '#root6',
watch: {
'deepMessage.a.b': 'handleMessage'
},
data() {
return {
deepMessage: { a: { b: 'Hello Vue' } },
copyMessage: ''
}
},
methods: {
handleMessage(value) {
this.copyMessage = value
}
}
})
</script>
</body>
</html>
class和style绑定
这里的mix方法里面用了三点运算符 学习一下
2.6新特性Vue.observable
可以方便的使用响应式属性, vue中使用响应式属性的话通常是要用vuex这个解决方案,来做集中的状态管理,
其实是对原来的observer方法做了重新的封装,让他能够在全局进行使用
应用比较简单的话直接就用observable就可以了 就不用vuex了
<html>
<head>
<title>Vue.observable</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<div id="root">
{{message}}
<button @click="change">Change</button>
</div>
<script>
const state = Vue.observable({ message: 'Vue 2.6' })
const mutation = {
setMessage(value) {
state.message = value
}
}
new Vue({
el: '#root',
computed: {
message() {
return state.message
}
},
methods: {
change() {
mutation.setMessage('Vue 3.0')
}
}
})
</script>
</body>
</html>
新特性2slot插槽
<html>
<head>
<title>插槽 slot</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<div id="root">
<div>案例1:slot的基本用法</div>
<Test>
<template v-slot:header="{user}">
<div>自定义header({{user.a}})</div>
</template>
<template v-slot="{user}">
<div>自定义body({{user.b}})</div>
</template>
</Test>
</div>
<div id="root2">
<div>案例2:Vue2.6新特性 - 动态slot</div>
<Test>
<template v-slot:[section]="{section}">
<div>this is {{section}}</div>
</template>
</Test>
<button @click="change">switch header and body</button>
</div>
<script>
Vue.component('Test', {
template:
'<div>' +
'<slot name="header" :user="obj" :section="\'header\'">' +
'<div>默认header</div>' +
'</slot>' +
'<slot :user="obj" :section="\'body\'">默认body</slot>' +
'</div>',
data() {
return {
obj: { a: 1, b: 2 }
}
}
})
new Vue({ el: '#root' })
new Vue({
el: '#root2',
data() {
return {
section: 'header'
}
},
methods: {
change() {
this.section === 'header' ?
this.section = 'default' :
this.section = 'header'
}
}
})
</script>
</body>
</html>
第六章vue router & vuex使用方法(小慕读书)
因为这套老师讲的是原理,我用法还没学过,所以这里看的视频是小慕读书的
1 vue router
小例子
http://www.youbaobao.xyz/mpvue-docs/guide/base_vuex.html
- index.html:应用的入口文件
- main.js:主js文件,初次渲染时执行
- App.vue:根组件,在main.js中加载
vue router解决了什么问题? 解决了路由与组件的对应关系
cnpm i -S vue-router 意思是保存到dependencies vue-router是运行时仍然需要使用的库 然后通过vue的use方法来加载插件 第三步 初始化一个vue-router的对象 第四步 实例化vue对象,传入router参数 之后就可以通过router-view和router-link两个官方组件来使用vue-router
在main.js(src下)更改为以下代码
import Vue from 'vue'
import App from './App.vue'
import Router from 'vue-router'
import A from './components/A.vue'
import B from './components/B.vue'
import HelloWorld from './components/HelloWorld.vue'
Vue.config.productionTip = false
Vue.use(Router)
const routes=[
{path:'/a',component:A},
{path:'/b',component:B},
{path:'/hello',component:HelloWorld}
]
const router=new Router({
routes
})
new Vue({
router,
render: h => h(App),
}).$mount('#app')
跳转
// eslint-disable-next-line
<template>
<div id="app">
<img alt="Vue logo" src="./assets/logo.png">
<!-- <HelloWorld msg="Welcome to Your Vue.js App"/> -->
<div>
<div>
<router-link to="/a">点我进入a</router-link>
</div>
<div>
<router-link to="/b">点我进入B</router-link>
</div>
<div>
<router-link to="/hello">点我进入 hello</router-link>
</div>
</div>
<router-view></router-view>
</div>
</template>
<script>
import HelloWorld from './components/HelloWorld.vue'
export default {
name: 'App',
components: {
HelloWorld
}
}
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
嵌套路由与重定向
嵌套路由应用场景: 左侧有个侧边栏 上面有个导航栏, 这里的/a 可能对应的就是侧边栏, /a下面可能还有个属性叫/aa aa就是对应的实际的页面内容 这就需要使用路由嵌套来实现这个需求
const routes = [{
path: '/a',
component: A,
redirect: '/a/aa',
children: [
{
path: '/a/aa',
component: AA,
}]
}]
访问A的时候如何重定向到AA?
{
path:'/a',
component:A,
redirect:"/a/aa",
children:[
{
path:'/a/aa',
component:AA
}
]
},
vue router的路由参数与编程式导航
为了支持restful 形式路由以及更复杂的场景时,我们可以使用动态路由,定义路由时,在路由前加上冒号即可,我们先添加AA2组件,动态路由部分通过this.$route.params 进行接收:
什么是restful形式的路由呢 192.168.31.148:8080/#/a/13344444 后面的数字为商品的编号,我们需要将商品展示到页面上,这个id是不停的改变的,就是说这个路由是不停改变的(动态路由),那么动态路由该如何实现呢 首先需要一个组件来支持动态路由展示,因为虽然路由是在改变,但是对应的模板是有一定规律的,
const routes=[
{
path:'/a',
component:A,
redirect:"/a/aa",
children:[
{
path:'/a/aa',
component:AA
},
{
path:'/a/:id',
component:AA2
}
]
},
{path:'/b',component:B},
{path:'/hello',component:HelloWorld}
]
<div>{{$route.params.id}}</div> //组件内接收到数据
还有一种 传递一个url参数
http://localhost:8081/#/a/aa?message=123
如何拿到123呢 不用在main.js里配置
<template>
<div>
我是a的子组件
<div>{{$route.query.message}}</div>
</div>
</template>
编程式路由: 有很多时候我们需要手动操作路由的跳转,这时我们需要使用this.$router ,以下是一些常用的操作: 不如说在b组件中增加一个按钮 点击这个按钮希望能够跳转到a页面
jump(){
this.$router.push('/a')
}
this.$router.push('/a/aa')
this.$router.push({
path: '/a/aa',
query: {
message: 'hello'
}
})
this.$router.replace('/a/123')
this.$router.go(-1)
2 vuex
vuex解决状态管理的问题,通过集中管理状态,使得state(date),actions(method),view(template)实现松耦合,让代码更容易维护,
const store=new Vuex.store({
state:{
data:"this is a data"
},
mutations:{
SET_DATA(state,data){
state.data=data
}
},
actions:{
setData({commit},data){
commit('SET_DATA',data)
}
}
})
<div>{{$store.state.data}}</div> 读取状态
修改:
update(){
this.$store.dispatch('setData',"update")
}
这样就实现了视图和状态的解耦
实际项目开发中,状态众多,如果全部混在一起,则难以分辨,而且容易相互冲突,为了解决问题,vuex引入模块化的概念,解决这个问题,下面我们定义a和b两个模块:
const moduleA={
state:{
data:"this is a"
},
mutations:{
SET_DATA(state,data){
state.data=data
}
},
actions:{
setData({commit},data){
commit('SET_DATA',data)
}
}
}
const moduleB={
state:{
data:"this is b"
},
mutations:{
SET_DATA(state,data){
state.data=data
}
},
actions:{
setData({commit},data){
commit('SET_DATA',data)
}
}
}
const store=new Vuex.Store({
modules:{
a:moduleA,
b:moduleB
}
})
<div>{{$store.state.a.data}}</div>
<div>{{$store.state.b.data}}</div>
<button @click="update('a')">update a</button>
<button @click="update('b')">update b</button>
update(ns) {
this.$store.dispatch(`setData`, `update ${ns}`)
}
上述代码在执行过程中,获取状态没有问题,但是修改状态会出现问题,因为两个模块出现同名actions,所以此时需要使用命名空间来解决这个问题:
const moduleA = {
namespaced: true,
}
update(ns) {
this.$store.dispatch(`${ns}/setData`, `update ${ns}`)
}
第五章element-UI用法
https://element.eleme.cn/#/zh-CN/component/installation
https://www.youbaobao.xyz/admin-docs/guide/base/element.html
基本用法
import ElementUI from 'element-ui'
Vue.use(ElementUI)
这时候写一个el-button 按钮是没有样式的,需要引入样式 它包含elementUI所有的样式
import 'element-ui/lib/theme-chalk/index.css'
写一个小case
给按钮增加一个点击事件,
<el-button @click="show">这是一个按钮</el-button>
show(){
this.$message.success('element-ui提示')
}
按需加载
为什么需要按需加载 我们对项目进行打包 npm run build 像我们这样全量引用的方式构建完之后的体积有多大呢
linux下的ll相当于windows下的ls
因为做了全量引用,所以占用空间比较大 未使用按需加载
现在需要把它改造成按需加载 先安装一个babel的插件
npm install babel-plugin-component -D
-D代表在develop下面创建插件
安装完之后就能做一个修改 生成了一个babel.config.js 打开它
{
"presets": [["es2015", { "modules": false }]],
"plugins": [
[
"component",
{
"libraryName": "element-ui",
"styleLibraryName": "theme-chalk"
}
]
]
}
增加一个"plugins"
"plugins": [
[
"component", 这是我们安装的插件
这些是component的配置信息
{
这个是指定按需加载的library的名称为element-ui
"libraryName": "element-ui",
样式库也做一个指定
"styleLibraryName": "theme-chalk"
}
]
]
这样的话那个import ‘element-ui/lib/theme-chalk/index.css’ 这个就可以去掉了
module.exports = {
presets: ["@vue/cli-plugin-babel/preset"],
plugins: [
[
"component",
{
libraryName: "element-ui",
styleLibraryName: "theme-chalk"
}
]
]
}
import Vue from 'vue'
import App from './App.vue'
import {Button,Message} from 'element-ui'
Vue.config.productionTip = false
Vue.component(Button.name,Button)
Vue.prototype.$message=Message
new Vue({
render: h => h(App),
}).$mount('#app')
更方便-插件引用
我们不用手动的编写babel文件,手动使用element-ui的按需引用 直接可以使用vuecli的插件来完成引用
表单的基本用法
有两个组件用起来比较复杂,表单组件和table组件
https://element.eleme.cn/#/zh-CN/component/form
https://www.youbaobao.xyz/admin-docs/guide/base/element.html#%E8%A1%A8%E5%8D%95%E5%9F%BA%E6%9C%AC%E7%94%A8%E6%B3%95
- el-form 容器,通过 model 绑定数据
- el-form-item 容器,通过 label 绑定标签
- 表单组件通过 v-model 绑定 model 中的数据
el-form :model=“xxx” el-from-item label= el-input/select… v-model=“xxx.yy”
如果不需要表单容器,使用div也是可以的 el-form-item也可以自己写 那么表单为什么要增加el-form 和el-form item呢,最重要的功能是表单的校验 还有对表单进行控制
比如说不想让用户名为空
onSubmit() {
console.log(this.data)
if(!this.data.user){
this.$message.error("用户名不能为空");
}
}
但是如果这样的话在这个方法里面要做的判断非常多,需要做大量的校验逻辑 我们这个方法应该是一个提交功能,并不是想做一个校验功能, 校验应该是validate的功能,有什么方法可以简化校验过程呢,el-form的价值就体现出来了,他提供了一套校验的解决方案。 他通过rules这个属性绑定到一个对象 对象的前面需要有个属性
注意这里截图有个地方写错了 props没有:
<template>
<div id="app">
<el-form inline :model="data" :rules="rules">
<!-- 注意这里的prop没加: -->
<el-form-item label="审批人" prop="user">
<el-input v-model="data.user" placeholder="审批人"></el-input>
</el-form-item>
<el-form-item label="活动区域">
<el-select v-model="data.region" placeholder="活动区域">
<el-option label="区域一" value="shanghai"></el-option>
<el-option label="区域二" value="beijing"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit">查询</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
export default {
name: "app",
data() {
const userValidator = (rule, value, callback) => {
// rule :当前校验规则,value:输入的值 ,
if (value.length > 3) {
callback();
} else {
callback(new Error("用户名长度必须大于3"));
}
};
return {
//这个data是表单的数据源 通过model进行绑定
data: {
user: "sam",
region: "区域二",
},
rules: {
user: [
{ required: true, trigger: "change", message: "用户名必须录入" },
{ validator: userValidator, trigger: "change" },
],
},
};
},
methods: {
onSubmit() {
console.log(this.data);
if (!this.data.user) {
this.$message.error("aaa");
}
},
},
};
</script>
那么错误如何被打印出来呢 方便对异常进行处理
两种方法一种通过绑定rules对change或者blur事件监听 另一种用el-form的api validate来实现手动校验(提交的时候做校验)
表单校验高级用法
1动态添加校验规则
动态添加校验规则 我们让rules只有一条校验规则,然后创建一个新的按钮
<template>
<div id="app">
<!-- validate-on-rule-change当rules发生变化会立即进行校验 -->
<el-form
inline
:model="data"
:rules="rules"
ref="form"
:validate-on-rule-change="false"
>
<!-- 注意这里的prop没加: -->
<el-form-item label="审批人" prop="user">
<el-input v-model="data.user" placeholder="审批人"></el-input>
</el-form-item>
<el-form-item label="活动区域">
<el-select v-model="data.region" placeholder="活动区域">
<el-option label="区域一" value="shanghai"></el-option>
<el-option label="区域二" value="beijing"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit()">查询</el-button>
<el-button type="primary" @click="addRule">点我添加校验规则</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
export default {
name: "app",
data() {
// const userValidator = (rule, value, callback) => {
// // rule :当前校验规则,value:输入的值 ,
// if (value.length > 3) {
// callback();
// } else {
// callback(new Error("用户名长度必须大于3"));
// }
// };
return {
//这个data是表单的数据源 通过model进行绑定
data: {
user: "sam",
region: "区域二",
},
rules: {
user: [
{ required: true, trigger: "change", message: "用户名必须录入" }, // blur
// { validator: userValidator, trigger: "change" },
],
},
};
},
methods: {
onSubmit() {
console.log(this.data);
// if (!this.data.user) {
// this.$message.error("aaa");
this.$refs.form.validate((res, err) => {
console.log(res, err);
});
},
addRule() {
const userValidator = (rule, value, callback) => {
if (value.length > 3) {
callback();
} else {
callback(new Error("用户名长度必须大于3"));
}
};
const newRule = [
// 拼接校验规则
...this.rules.user,
{ validator: userValidator, trigger: "change" },
];
// this.rules = Object.assign({}, this.rules, { user: newRule }); 这是老师的
// 我认为还可以这样写
// this.rules = Object.assign({}, { user: newRule });
// 或者下面这样
this.rules = { user: newRule };
// this.rules.user.push(newRule) //这个不行,因为watch监听不到user的变化 只能监听rules本身的变化
},
},
};
</script>
2手动控制校验状态
- validate-status:验证状态,枚举值,共四种:
- success:验证成功
- error:验证失败
- validating:验证中
- (空):未验证
- error:自定义错误提示
<template>
<div id="app">
<!-- validate-on-rule-change当rules发生变化会立即进行校验 -->
<el-form
inline
:model="data"
:rules="rules"
ref="form"
:validate-on-rule-change="false"
status-icon
>
<!-- 注意这里的prop没加: -->
<el-form-item
label="审批人"
prop="user"
:error="error"
:validate-status="status"
>
<el-input v-model="data.user" placeholder="审批人"></el-input>
</el-form-item>
<el-form-item label="活动区域">
<el-select v-model="data.region" placeholder="活动区域">
<el-option label="区域一" value="shanghai"></el-option>
<el-option label="区域二" value="beijing"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit()">查询</el-button>
<el-button type="primary" @click="addRule">点我添加校验规则</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
export default {
name: "app",
data() {
// const userValidator = (rule, value, callback) => {
// // rule :当前校验规则,value:输入的值 ,
// if (value.length > 3) {
// callback();
// } else {
// callback(new Error("用户名长度必须大于3"));
// }
// };
return {
//这个data是表单的数据源 通过model进行绑定
error: "",
status: "",
data: {
user: "sam",
region: "区域二",
},
rules: {
user: [
{ required: true, trigger: "change", message: "用户名必须录入" }, // blur
// { validator: userValidator, trigger: "change" },
],
},
};
},
methods: {
onSubmit() {
console.log(this.data);
// if (!this.data.user) {
// this.$message.error("aaa");
this.$refs.form.validate((res, err) => {
console.log(res, err);
});
},
addRule() {
const userValidator = (rule, value, callback) => {
if (value.length > 3) {
callback();
} else {
callback(new Error("用户名长度必须大于3"));
}
};
const newRule = [
// 拼接校验规则
...this.rules.user,
{ validator: userValidator, trigger: "change" },
];
// this.rules = Object.assign({}, this.rules, { user: newRule }); 这是老师的
// 我认为还可以这样写
// this.rules = Object.assign({}, { user: newRule });
// 或者下面这样
this.rules = { user: newRule };
// this.rules.user.push(newRule) //这个不行,因为watch监听不到user的变化 只能监听rules本身的变化
},
},
};
</script>
<template>
<div id="app">
<!-- validate-on-rule-change当rules发生变化会立即进行校验 -->
<el-form
inline
:model="data"
:rules="rules"
ref="form"
:validate-on-rule-change="false"
status-icon
>
<!-- 注意这里的prop没加: -->
<el-form-item
label="审批人"
prop="user"
:error="error"
:validate-status="status"
>
<el-input v-model="data.user" placeholder="审批人"></el-input>
</el-form-item>
<el-form-item label="活动区域">
<el-select v-model="data.region" placeholder="活动区域">
<el-option label="区域一" value="shanghai"></el-option>
<el-option label="区域二" value="beijing"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit()">查询</el-button>
<el-button type="primary" @click="addRule">点我添加校验规则</el-button>
<el-button type="success" @click="showSuccess">成功校验</el-button>
<el-button type="danger" @click="showError">失败校验</el-button>
<el-button type="warning" @click="showValidating">校验中</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
export default {
name: "app",
data() {
// const userValidator = (rule, value, callback) => {
// // rule :当前校验规则,value:输入的值 ,
// if (value.length > 3) {
// callback();
// } else {
// callback(new Error("用户名长度必须大于3"));
// }
// };
return {
//这个data是表单的数据源 通过model进行绑定
error: "",
status: "",
data: {
user: "sam",
region: "区域二",
},
rules: {
user: [
{ required: true, trigger: "change", message: "用户名必须录入" }, // blur
// { validator: userValidator, trigger: "change" },
],
},
};
},
methods: {
onSubmit() {
console.log(this.data);
// if (!this.data.user) {
// this.$message.error("aaa");
this.$refs.form.validate((res, err) => {
console.log(res, err);
});
},
addRule() {
const userValidator = (rule, value, callback) => {
if (value.length > 3) {
callback();
} else {
callback(new Error("用户名长度必须大于3"));
}
};
const newRule = [
// 拼接校验规则
...this.rules.user,
{ validator: userValidator, trigger: "change" },
];
// this.rules = Object.assign({}, this.rules, { user: newRule }); 这是老师的
// 我认为还可以这样写
// this.rules = Object.assign({}, { user: newRule });
// 或者下面这样
this.rules = { user: newRule };
// this.rules.user.push(newRule) //这个不行,因为watch监听不到user的变化 只能监听rules本身的变化
},
showError() {
this.status = "error";
this.error = "用户名输入有误";
},
showSuccess() {
this.status = "success";
this.error = "";
},
showValidating() {
this.status = "validating";
this.error = "";
},
},
};
</script>
表单常见属性(里面有provide和inject的知识点)
- label-position:标签位置,枚举值,left 和 top
- label-width:标签宽度
- label-suffix:标签后缀
- inline:行内表单
- disabled: 设置整个 form 中的表单组件全部 disabled,优先级低于表单组件自身的 disabled 这个不是通过遍历而是用provide 设计很巧妙
inputDisabled() {
return this.disabled || (this.elForm || {}).disabled;
}
inputSize() {
return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
},
_elFormItemSize() {
return (this.elFormItem || {}).elFormItemSize;
}
elFormItemSize() {
return this.size || this._formSize;
},
_formSize() {
return this.elForm.size;
}
el-form源码解析
略
案例
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vZpqxp2y-1628951502651)(https://files.catbox.moe/muvrfd.png)]
<template>
<div id="app">
<el-form
:model="data"
style="width: 500px"
label-position="left"
label-width="100px"
label-suffix=":"
:inline="false"
:rules="rules"
:disabled="false"
status-icon
validate-on-rule-change
hide-required-asterisk
:inline-message="false"
>
<el-form-item
label="用户名"
prop="user"
:error="error"
:validate-status="status"
>
<el-input v-model="data.user" placeholder="用户名" clearable></el-input>
</el-form-item>
<el-form-item label="活动区域" prop="region">
<el-select v-model="data.region" placeholder="活动区域" style="width:100%">
<el-option label="区域一" value="shanghai"></el-option>
<el-option label="区域二" value="beijing"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit">查询</el-button>
<el-button type="primary" @click="addRule">添加校验规则</el-button>
<el-button @click="showError">错误状态</el-button>
<el-button @click="showSuccess">正确状态</el-button>
<el-button @click="showValidating">验证状态</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
export default {
name: 'app',
data() {
return {
data: {
user: 'sam',
region: '区域二'
},
error: '',
status: '',
rules: {
user: [
{ required: true, trigger: 'change', message: '用户名必须录入' }
]
}
}
},
methods: {
onSubmit() {
console.log(this.data)
},
addRule() {
const userValidator = (rule, value, callback) => {
if (value.length > 3) {
callback()
} else {
callback(new Error('用户名长度必须大于3'))
}
}
const newRule = [
...this.rules.user,
{ validator: userValidator, trigger: 'change' }
]
this.rules = Object.assign({}, this.rules, { user: newRule })
},
showError() {
this.status = 'error'
this.error = '用户名输入有误'
},
showSuccess() {
this.status = 'success'
this.error = ''
},
showValidating() {
this.status = 'validating'
this.error = ''
}
}
}
</script>
第七章 前端框架搭建
git clone https://github.com/PanJiaChen/vue-element-admin.git
cnpm i npm run dev
- 删除 src/views 下的源码,保留:
- dashboard:首页
- error-page:异常页面
- login:登录
- redirect:重定向
- 对 src/router/index 进行相应修改
- 删除 src/router/modules 文件夹
- 删除 src/vendor 文件夹
如果是线上项目,建议将 components 的内容也进行清理,以免影响访问速度,或者直接使用 vue-admin-template 构建项目,课程选择 vue-element-admin 初始化项目,是因为 vue-element-admin 实现了登录模块,包括 token 校验、网络请求等,可以简化我们的开发工作
然后对src/router/index进行修改 module也给删了
这个是做登录的认证跳转
{
path: '/auth-redirect',
component: () => import('@/views/login/auth-redirect'),
hidden: true
},
删除 src/vendor 文件夹 这是转换成excel和zip的文件
src/setting.js: 里面有一系列的配置项
这里有个技巧,可以搜一下这个title在哪儿被引用了
还可以再看一下get-page-title在哪儿被引用了
permission.js
router.beforeEach(async(to, from, next) => {
NProgress.start()
document.title = getPageTitle(to.meta.title)
}
项目源码如何进行调试
修改 vue.config.js:
这一块老师本来就有 但是我的没有,所以给加上去了
将 cheap-source-map 改为 source-map,如果希望提升构建速度可以改为 eval
但是我加不加都一样,
这里同样显示的源码
通常建议开发时保持 eval 配置,以增加构建速度,当出现需要源码调试排查问题时改为 source-map
是我的操作方法不对吗,改成eval之后我这儿也能看到源码啊
这个layout很重要
我们自己写的组件其实是替换到了这里
路由嵌套:最外层有个大的容器:App.vue 它会被router-view进行替换,这个对应的是layout layout对应整个页面的框架
如果把这个app-main注释掉:
这里有个keep-alive 可以保证当前的组件被缓存,如果不需要缓存的话改怎么做呢
第八章 服务端框架搭建
https://github.com/expressjs/express
Node 是一个基于 V8 引擎的 Javascript 运行环境,它使得 Javascript 可以运行在服务端,直接与操作系统进行交互,与文件控制、网络交互、进程控制等
Chrome 浏览器同样是集成了 V8 引擎的 Javascript 运行环境,与 Node 不同的是他们向 Javascript 注入的内容不同,Chrome 向 Javascript 注入了 window 对象,Node 注入的是 global,这使得两者应用场景完全不同,Chrome 的 Javascript 所有指令都需要通过 Chrome 浏览器作为中介实现
npm init -y
npm i -S express
新建App.js(全局入口文件)
const express = require('express')
const app = express()
app.get('/', function(req, res) {
res.send('hello node')
})
const server = app.listen(5000, function() {
const { address, port } = server.address()
console.log('Http Server is running on http://%s:%s', address, port)
})
express基础概念
中间件,路由,异常处理
中间件
中间件是一个函数,在请求和响应周期中被顺序调用
Middleware functions are functions that have access to the request object (req), the response object (res), and the next function in the application’s request-response cycle.
const myLogger = function(req, res, next) {
console.log('myLogger')
next()
}
app.use(myLogger)
路由
应用如何响应请求的一种规则
Routing refers to how an application’s endpoints (URIs) respond to client requests.
响应 / 路径的 get 请求:
app.get('/', function(req, res) {
res.send('hello node')
})
响应 / 路径的 post 请求:
app.post('/', function(req, res) {
res.send('hello node')
})
规则主要分两部分:
- 请求方法:get、post…
- 请求的路径:/、/user、/.*fly$/…
异常处理
通过自定义异常处理中间件处理请求中产生的异常
app.get('/', function(req, res) {
throw new Error('something has error...')
})
const errorHandler = function (err, req, res, next) {
console.log('errorHandler...')
res.status(500)
res.send('down...')
}
app.use(errorHandler)
TIP
使用时需要注意两点:
- 第一,参数一个不能少,否则会视为普通的中间件
- 第二,中间件需要在请求之后引用
项目框架搭建
安装 boom 依赖:
npm i -S boom
创建 router 文件夹,创建 router/index.js:
const express = require('express')
const boom = require('boom')
const userRouter = require('./user')
const {
CODE_ERROR
} = require('../utils/constant')
const router = express.Router()
router.get('/', function(req, res) {
res.send('欢迎学习小慕读书管理后台')
})
router.use('/user', userRouter)
router.use((req, res, next) => {
next(boom.notFound('接口不存在'))
})
router.use((err, req, res, next) => {
const msg = (err && err.message) || '系统错误'
const statusCode = (err.output && err.output.statusCode) || 500;
const errorMsg = (err.output && err.output.payload && err.output.payload.error) || err.message
res.status(statusCode).json({
code: CODE_ERROR,
msg,
error: statusCode,
errorMsg
})
})
module.exports = router
创建 router/use.js:
const express = require('express')
const router = express.Router()
router.get('/info', function(req, res, next) {
res.json('user info...')
})
module.exports = router
创建 utils/constant:
module.exports = {
CODE_ERROR: -1
}
验证 /user/info:
"user info..."
验证 /user/login:
{“code”:-1,“msg”:“接口不存在”,“error”:404,“errorMsg”:“Not Found”}
boom依赖可以帮助我们快速的生成异常信息
router.use((err, req, res, next) => {
const msg = (err && err.message) || '系统错误'
const statusCode = (err.output && err.output.statusCode) || 500;
const errorMsg = (err.output && err.output.payload && err.output.payload.error) || err.message
res.status(statusCode).json({
code: CODE_ERROR,
msg,
error: statusCode,
errorMsg
})
})
第九章 项目架构解析
项目需求分析
登录
- 用户名密码校验
- token 生成、校验和路由过滤
- 前端 token 校验和重定向
token可以防止我们的请求被窃取 只有通过token才能完成后面的请求 生成token我们用到了jwt技术来实现。我们还要完成前后端的token校验以及路由过滤。路由过滤是指哪些请求是需要传递token的,哪些不需要传就可以直接访问。
我们在前端校验token的时候还要注意,当token校验不通过的时候该如何进行处理呢–给用户一个登陆失效的提示,并且重定向把用户引导到首页当中,如果不重定向的话因为页面没有退出,所以会看到一些异常的页面
生成token的时候还会有些细节 ,比如说多长时间更新一次
电子书上传
- 文件上传
- 静态资源服务器 如何将字节码转换成资源文件 用到了nodejs中的fs(file system)
电子书解析
-
epub原理 -
zip解压 -
xml解析 epub的本质是一个压缩文件,
电子书增删改
- mysql数据库应用
- 前后端异常处理(查不到数据怎么办 接口返回报错以后怎么办)
主要是图书列表页面
里面有翻页控制啊,查询啊一些细节
主要联系mysql数据库应用
电子书解析
修改为.zip
html文件对应的是每个章节的信息
mimetype
vim mimetype --资源类型 application/epub/zip
如何解析?
META-INF / container.xml
这个文件规定了外层为container 里面叫rootfiles(数组) 这个数组当中包含了对象 rootfile 需要包含两个属性 一个是full-path content.opf(重要) ,media-type规定了content.opf是个什么类型
找到content.opf 也是一个opf文件
这里的spin是阅读顺序 根据idref到manifest里面找路径
然后guide是个导读,很多电子书是没有的
还有一个很重要的地方 spin后面有个toc=“ncx” 意思是在项目当中还会包含一个toc.ncx文件,这个文件是我们的目录文件指明了目录文件,后面对目录进行解析的时候就是解析这个 重点是navMap
nginx服务器配置
server {
charset utf-8;
listen 8089;
server_name http_host;
root E:\\Compressed\\nginx-1.21.1\\epub;
autoindex on;
add_header Cache-Control "no-cache, must-revalidate";
location / {
add_header Access-Control-Allow-Origin *;
}
}
autoindex:是否打开一个索引
add_header Cache-Control “no-cache, must-revalidate”;:不使用缓存 用户请求服务端,这个缓存是在本地,不是在服务端做缓存,是在客户端做缓存,如果说设一个小时意味着客户端一个小时不会向服务端发起请求 有好有坏
location / { add_header Access-Control-Allow-Origin *; }
这是一个路由 / 表示监听所有的路由,对所有的路由都生效, 对所有的路由都增加一个add_header 这个header是Access-Control-Allow-Origin 防止跨域 本地配置成*(任何人都能来访问) 实际上线了以后要改成一个允许的域名
静态资源服务器nginx启动
switchhost
https://blog.csdn.net/liangxw1/article/details/78661112
访问到线上,如何在本地也访问到呢, /etc/hosts 通过修改这个文件来实现 不方便
switchhost 快速添加删除host 访问域名的时候都会映射到本地 而且连接也是安全的
事实上,在大厂开发的时候都是通过这种方式,大厂的所有接口他的跨域限制做的是非常严格的 必须通过制定域名才能访问到资源,如果不通过这个域名是没办法访问的 这时候需要对资源进行封装,如果通过localhost是访问不到资源的 ,还有个好处是域名和线上是相同的,意味着现在的环境和线上是近视的,线上的时候是通过这个url来访问资源的
<strong>注意不要直接双击nginx.exe,这样会导致修改配置后重启、停止nginx无效,需要手动关闭任务管理器内的所有nginx进程
在nginx.exe目录,打开命令行工具,用命令 启动/关闭/重启nginx
start nginx : 启动nginx
nginx -s reload :修改配置后重新加载生效
nginx -s reopen :重新打开日志文件
nginx -t -c /path/to/nginx.conf 测试nginx配置文件是否正确
关闭nginx:
nginx -s stop :快速停止nginx
nginx -s quit :完整有序的停止nginx
如果遇到报错:
bash: nginx: command not found
有可能是你再linux命令行环境下运行了windows命令,
如果你之前是允许 nginx -s reload报错, 试下 ./nginx -s reload
或者 用windows系统自带命令行工具运行</strong>
mysql
记得建数据库的时候选择utf-8
第十章 登录功能开发上
登录流程分析
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bUS95wzM-1628951502683)(https://www.youbaobao.xyz/admin-docs/assets/img/login_process.58cab9a5.png)]
各种简化
- 删除 SocialSign 组件引用
- 删除 src/views/login/components 目录
- 删除 afterQRScan
- 删除 created 和 destroyed
准备工作:简化代码,如何格式化(webstorm)以及indent报错问题
预备知识
路由和权限校验
https://blog.csdn.net/xiaozhazhazhazha/article/details/118862475
详见 router
为什么要做权限校验呢,因为访问login这个路由的时候 vue-element-admin这个框架是做了些校验的 比如说登录到dashboard之后 此时将token删除的话,这时候刷新页面会看到是会退回login页面的,说明框架本身做了些事情,就是进行权限校验,权限校验是放在那里做呢 是放在路由当中做的。
这里有个具体的实例
创建组件
创建组件 src/views/book/create.vue
组件创建完毕 但是还没有办法 访问到 因此要到router中做一些配置,
配置路由
修改 src/router/index.js 的 asyncRoutes:
export const asyncRoutes = [
{
path: '/book',
component: Layout,
redirect: '/book/create',
children: [
{
path: '/book/create',
component: () => import('@/views/book/create'),
name: 'book',
meta: { title: '添加图书', icon: 'edit', roles: ['admin'] }
}
]
},
]
- 使用 editor 登录平台,无法看到"添加图书"功能
- 使用 admin 登录平台,可以看到"添加图书"功能
篇幅有点长,放到csdn了 https://blog.csdn.net/xiaozhazhazhazha/article/details/118862475
梳理总结
关于路由处理
- vue-element-admin 对所有访问的路由进行拦截;
- 访问路由时会从 Cookie 中获取 Token,判断 Token 是否存在:
- 如果 Token 存在,将根据用户角色生成动态路由,然后访问路由,生成对应的页面组件。这里有一个特例,即用户访问
/login 时会重定向至 / 路由;/路由也会重定向到dashboard - 如果 Token 不存在(没有登录),则会判断路由是否在白名单中,如果在白名单中将直接访问,否则说明该路由需要登录才能访问,此时会将路由生成一个 redirect 参数传入 login 组件,实际访问的路由为:
/login?redirect=/xxx 登录之后会做一个重定向,重定向到xxx 重定向怎么实现的在10.13
关于动态路由和权限校验
-
vue-element-admin 将路由分为:constantRoutes 和 asyncRoutes 用户登录系统的时候会动态生成路由表 -
用户登录系统时,会动态生成路由,其中 constantRoutes 必然包含,asyncRoutes 会进行过滤; -
asyncRoutes 过滤的逻辑是看路由下是否包含 meta 和 meta.roles 属性,如果没有该属性,所以这是一个通用路由,不需要进行权限校验,会被加到路由表中;如果包含 roles 属性则会判断用户的角色是否命中路由中的任意一个权限,如果命中,则将路由保存下来,如果未命中,则直接将该路由舍弃; -
asyncRoutes 处理完毕后,会和 constantRoutes 合并为一个新的路由对象,并保存到 vuex 的 permission/routes 中; -
用户登录系统后,侧边栏会从 vuex 中获取 state.permission.routes ,根据该路由动态渲染用户菜单。
一定好好掌握,这是整个框架的精髓所在
侧边栏
https://blog.csdn.net/xiaozhazhazhazha/article/details/118873782
详见 sidebar
重定向
https://blog.csdn.net/xiaozhazhazhazha/article/details/118887770
详见 redirect
面包屑导航
https://blog.csdn.net/xiaozhazhazhazha/article/details/118974389
详见 breadcrumb
第十一章 登录(中)
登录组件分析
src/views/login
可以通过validator手动添加校验
$ref.ref的名字.validate来输出信息
el-tooltip:https://element.eleme.cn/#/zh-CN/component/tooltip#tooltip-wen-zi-ti-shi
.native什么意思? 在自定义组件,如果要调用原生input上面就要用native,就是说要绑定到原生的input组件空间上去
判断大写:
checkCapslock(e) {
const { key } = e
this.capsTooltip = key && key.length === 1 && (key >= 'A' && key <= 'Z')
},
showPwd() {
if (this.passwordType === 'password') {
this.passwordType = ''
} else {
this.passwordType = 'password'
}
this.$nextTick(() => {
this.$refs.password.focus()
})
},
handleLogin:
this.$store.dispatch(‘user/login’, this.loginForm) 进入的文件
login 里面又传入了一个login方法
他的前缀是什么
可以看一下
然后resolve 到.then的这一部分
axios用法分析
官网http://www.axios-js.com/zh-cn/docs/
给我们封装好的request库
request 库使用了 axios 的手动实例化方法 create 来封装请求,要理解其中的用法,我们需要首先学习 axios 库的用法
来学习一下axios用法
import axios from 'axios'
const url = 'https://test.youbaobao.xyz:18081/book/home/v2?openId=1234'
axios.get(url).then(response => {
console.log(response)
})
其实可以把参数放到params里
const url = 'https://test.youbaobao.xyz:18081/book/home/v2'
axios.get(url, {
params: { openId: '1234' }
})
这里老师的接口不能用,我就用自己的了
http://47.103.29.206:3000/mv/first?limit=10
token是在header里面写的
写上token以后不知道为啥会出现跨域问题
我知道了 又看了一下官方文档
需要加上这一句xhrFields: { withCredentials: true }
不行 还是有错
不管他了 这个接口不知道怎么设跨域 后面写自己的接口的时候在设吧
如果服务端抛了个异常 比如404 (非200)可以通过catch来捕获异常
这样写是可以,但是呢会造成冗余代码,不方便维护,如果生成token的逻辑一旦变化,
- 每个需要传入 token 的请求都需要添加 headers 对象,会造成大量重复代码
- 每个请求都需要手动定义异常处理,而异常处理的逻辑大多是一致的,如果将其封装成通用的异常处理方法,那么每个请求都要调用一遍
我们希望发一次请求,请求的时候能够拦截header生成的过程,在header生成过程中把token给他插入进去
如果说get写一套,post写一套,其他的也写几套 其实也是会冗余的
下面来学习一下axios实例化,通过调用create方法来解决这个问题
axios.create返回的结果是个function,他不是个具体的结果,在这里,可以给他内置一些基础参数,比如baseUrl,timeout
所以说我们就可以给固定的部分生成一个构造函数
通过request函数再来发起请求 其中,他的基础参数是从create的时候传入的
created() {
const url='/search'
const request=axios.create({
baseURL:'http://47.103.29.206:3000',
timeout:5000
})
request({
url,
params:{
keywords:'%E6%B5%B7%E9%98%94%E5%A4%A9%E7%A9%BA'
}
}).then((res)=>{
console.log(res);
})
},
axios有更高级的功能,拦截器
上述代码完成了基本请求的功能,下面我们需要为 http 请求的 headers 中添加 token,同时进行白名单校验,如 /login 不需要添加 token,并实现异步捕获和自定义处理
created() {
const whileUrl=['/login','/search']
const url='/search'
const request=axios.create({
baseURL:'http://47.103.29.206:3000',
timeout:5000
})
request.interceptors.request.use(
config=>{
console.log(config);
if(whileUrl.some(wl=>url===wl)){
return config
}
config.headers['token']='abcd'
return config
},
err=>{
console.log(err);
}
)
request({
url,
params:{
keywords:'%E6%B5%B7%E9%98%94%E5%A4%A9%E7%A9%BA'
}
}).then((res)=>{
console.log(res);
})
},
可以尝试一下在白名单里删除search 发现会报跨域错误,就是说本来应该是加上了token的
有什么实际意义呢
我们在实际项目发起请求的时候login请求往往是不需要加token的
所以这时候就可以加一个白名单,白名单这个方法没有token也可以进入
白名单以外的进入都要加token
我们可以抛个异常,然后对他进行异常处理
也可以通过Promise.reject(err)把err注入到reject参数里面,这样就可以在reject之后通过catch方法来自己处理这个异常
然后就可以在catch里面做进一步操作
created() {
const whileUrl = ["/login", "/search"];
const url = "/search";
const request = axios.create({
baseURL: "http://47.103.29.206:3000",
timeout: 5000,
});
request.interceptors.request.use(
(config) => {
console.log(config);
if (whileUrl.some((wl) => url === wl)) {
return config;
}
config.headers["token"] = "abcd";
return config;
},
(err) => {
console.log(err);
Promise.reject(err)
}
);
request({
url,
params: {
keywords: "%E6%B5%B7%E9%98%94%E5%A4%A9%E7%A9%BA",
},
}).then((res) => {
console.log(res);
}).catch((err)=>{
console.log(err);
})
},
响应拦截器
request.interception.response.use
在里面可以做一系列判断,比如error_code
第一个参数 :response 第二个:err
打印一下response
之后就可以判断里面的error_code
data.code
created() {
const whileUrl = ["/login", "/search"];
const url = "/search";
const request = axios.create({
baseURL: "http://47.103.29.206:3000",
timeout: 5000,
});
request.interceptors.request.use(
(config) => {
console.log(config);
if (whileUrl.some((wl) => url === wl)) {
return config;
}
config.headers["token"] = "abcd";
return config;
},
(err) => {
console.log(err);
Promise.reject(err)
}
);
request.interceptors.response.use(
response=>{
console.log(response);
if(response.data&&response.data.code ===200){
return response.data
}else{
Promise.reject(response.data.code)
}
},
error=>{
Promise.reject(error)
}
)
request({
url,
params: {
keywords: "%E6%B5%B7%E9%98%94%E5%A4%A9%E7%A9%BA",
},
}).then((res) => {
console.log(res);
}).catch((err)=>{
console.log(err);
})
},
request库源码分析
https://blog.csdn.net/xiaozhazhazhazha/article/details/119065931
登录细节分析
进入页面默认聚焦用户名密码输入框:
if (this.loginForm.username === '') {
this.$refs.username.focus()
} else if (this.loginForm.password === '') {
this.$refs.password.focus()
}
显示密码后自动聚焦
showPwd() {
if (this.passwordType === 'password') {
this.passwordType = ''
} else {
this.passwordType = 'password'
}
this.$nextTick(() => {
this.$refs.password.focus()
})
},
用reduce进行过滤
getOtherQuery(query) {
return Object.keys(query).reduce((acc, cur) => {
if (cur !== 'redirect') {
acc[cur] = query[cur]
}
return acc
}, {})
}
关闭mock接口 修改接口地址
main.js 删掉mock相关代码
api里面的artical qiniu删掉不需要
删除vue.config.js相关配置
把有关mock的删掉
之后 .env.development
对baseapi做个修改
把production的api也改成这个
把当前的域名映射到本地
之后点开网站看看
这是因为request.js里面的baseurl变了
老师的是这样的
比我少了一个vue-element-admin
这里的问题 给他删掉
接着可以去开发接口了
第十二章 登录(下)
node+mysql数据库,前后端联动,md5加密
搭建http服务
搭建https服务的话:搭建https服务
http服务我们前面其实已经搭好了
现在把端口换一下 18082
因为访问47.103.29.206:18082的话会报错,因为已经注册过了 所以我后面加了个a
const express=require('express')
const router=require('./router')
const app=express()
app.use('/',router)
app.listen(18082,()=>{
console.log("http://localhost:18082");
})
登录api开发
在user里创建一个api
随便写点测试一下
router.post('/login',(req,res)=>{
console.log(req.body);
res.json({
code:0,
msg:'登录成功'
})
})
因为浏览器不好做post请求测试
这里可以用postman
也可以下载一个curl在dos窗口
或者git bash里面输入也可以
curl http://47.103.29.206a:18082/user/login -X POST
输出req.body为undifined 说明并没有传入参数
可以通过-d来指定body里的参数
curl http://47.103.29.206a:18082/user/login -X POST -d 'username=admin&password=1234'
但是body里面的参数仍然没有被解析 需要用到body-parser中间件来解决这个问题
安装body-parser cnpm i -S body-parser
关于body-parser的用法 :https://juejin.cn/post/6844903478830055431
不用下这个包了 已经被弃用了
直接用express调用bodyParser的方法就可以了
发的请求可以简化一下 ,加上-d以后他默认是post
curl http://47.103.29.206a:18082/user/login -d 'username=admin&password=1234'
记得把前端的baseAPI也改了
然后运行一下前端项目
到时候上线了就把a给去掉
跨域问题
所以在服务端写个解决跨域的方案
可以看到参数已经传进来了
同时还收到了返回值
这时候前端后端已经发生了联动了 可以互通了
这里我们在 Network 中会发现发起了两次 https 请求,这是因为由于触发跨域,所以会首先进行 OPTIONS 请求,判断服务端是否允许跨域请求,如果允许才能实际进行请求
也就是说跨域请求的访问是由服务端控制的,服务端下发了这样的response header之后,
浏览器就知道是可以发送请求的,如果跨域条件不满足的时候,浏览器就直接抛出异常
响应结果封装
const {
CODE_ERROR,
CODE_SUCCESS
} = require('../utils/constant')
class Result {
constructor(data, msg = '操作成功', options) {
this.data = null
if (arguments.length === 0) {
this.msg = '操作成功'
} else if (arguments.length === 1) {
this.msg = data
} else {
this.data = data
this.msg = msg
if (options) {
this.options = options
}
}
}
createResult() {
if (!this.code) {
this.code = CODE_SUCCESS
}
let base = {
code: this.code,
msg: this.msg
}
if (this.data) {
base.data = this.data
}
if (this.options) {
base = { ...base, ...this.options }
}
console.log(base)
return base
}
json(res) {
res.json(this.createResult())
}
success(res) {
this.code = CODE_SUCCESS
this.json(res)
}
fail(res) {
this.code = CODE_ERROR
this.json(res)
}
}
module.exports = Result
还要来改造一下user.js
router.post('/login',(req,res)=>{
console.log(req.body);
new Result('登录成功').success(res)
})
做失效功能的时候只需要增加一个方法就行 扩展能力很好
如果success里面出现了异常,可以在index.js的异常处理语句中被处理
mysql
文档
密码是用的md5加盐
我们需要在node中安装mysql数据库
建一个db文件夹 专门存储db的相关操作
里面包含两个文件 config.js index.js
config.js:
module.exports={
host:'localhost',
user:'root',
password:'111111',
database:'book'
}
index.js
const mysql = require('mysql')
const config = require('./config')
function connect() {
return mysql.createConnection({
...config
})
}
function querySql(sql) {
const conn = connect()
return new Promise((resolve, reject) => {
try {
conn.query(sql, (err, results) => {
if (err) {
reject(err)
}
resolve(results)
})
}
catch (e) {
reject(e)
}
finally {
conn.end()
}
})
}
module.exports=querySql
user.js引入
const querySql=require('../db/index')
querySql('select * from admin_user').then((res)=>{
console.log(res);
}).catch((err)=>{
console.log(err);
})
有个技巧,我们可以在关键的地方打印一些参数, 在constant里面写个debug为true
然后在需要输出的页面里引入debug
debug&&console.log(xxxx)
在上线的时候我们就可以把debug改成false 就不会再打印了
这是在db/index.js里面写的, 就不用再业务逻辑里面打印了
这个配置文件可以从代码中抽离出来,变成一个单独的配置文件,这样就可以在不启动代码的情况下实现debug的切换
有了这个查询语句 ,就可以对用户查询进一步封装
我们是直接将查询语句写在user.js里了
更好的做法是再建一个service层 把业务逻辑全放到service层
创建service 里面包含了个user.js
创建一个function 叫login
const querySql=require('../db/index')
function login(username,password){
return querySql(`select * from admin_user where username='${username}' and password='${password}'`)
}
module.exports=login
在router/user.js
const { username, password } = req.body
login(username, password).then((user) => {
if (!user || user.length == 0) {
new Result('登录失败').fail(res)
} else {
new Result('登录成功').success(res)
}
})
这时,即使我们输入正确的用户名密码也会出现无法登陆的情况
这是因为密码采用了MD5+SALT加密,所以需要对密码进行对等加密。
去constant文件
加上这句话
PWD_SALT:'admin_imooc_node'
这个就相当于一个秘钥,通过秘钥和密码混合之后生成新的密码
不知道秘钥的话外界要破解这个密码是很难破解的
接着对密码进行MD5加密
cnpm i -S crypto
新建文件utils/index.js 并写入
const crypto=require('crypto')
function md5(s){
return crypto.createHash('md5').update(String(s)).digest('hex')
}
module.exports=md5
接着要对password进行改造
password=md5(`
p
a
s
s
w
o
r
d
{password}
password{PWD_SALT}`)
express-validator
https://express-validator.github.io/docs/validation-chain-api.html
https://blog.csdn.net/cuk0051/article/details/108343329
用处:可以简化对post请求的校验
可以帮助我们快速的验证表单当中的参数
安装依赖
使用它的body方法,在post请求当中,需要在第二个参数当中传入一个数组,通过数组来进行校验,
如何接受到参数呢
这里的err有个isEmpty方法,这个方法可以去判断数组是否为空
如果isEmpty为false表示出现了验证错误,我们就可以从errors里面拿到msg然后返回给前端抛出异常
然后通过第三个参数next来继续传递,传递给下一个中间件来执行
这里的[ ]=err.errors代表取出第一个元素
去前端测试一下
注意next 传递给下一个中间件,下一个中间件是
express-validator 使用技巧:
- 在
router.post 方法中使用 body 方法判断参数类型,并指定出错时的提示信息 - 使用
const err = validationResult(req) 获取错误信息,err.errors 是一个数组,包含所有错误信息,如果 err.errors 为空则表示校验成功,没有参数错误 - 如果发现错误我们可以使用
next(boom.badRequest(msg)) 抛出异常,交给我们自定义的异常处理方法进行处理
jwt
https://www.youbaobao.xyz/admin-docs/guide/extra/jwt.html
https://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html
https://github.com/dwyl/learn-json-web-tokens/blob/master/README.md
基本概念
Token 的用途主要有三点:
- 拦截无效请求,降低服务器处理压力;
- 实现第三方 API 授权,无需每次都输入用户名密码鉴权;
- 身份校验,防止 CSRF 攻击
可以在https://jwt.io/ 调试jwt字符串
有三段,第一段是加密的算法和token的类型,第二段是具体数据,第三段是签名部分,使用加密算法
私钥是存在服务端的,别人不知道私钥的时候是无法破解里面的信息的,最后解开的时候是要用到私钥的。
生成jwt token
npm i -S jsonwebtoken
需要一个私钥和一个过期时间,过期时间不宜过短,也不宜过长,课程里设置为 1 小时,实际业务中可根据场景来判断,通常建议不超过 24 小时,保密性要求高的业务可以设置为 1-2 小时:
const jwt = require('jsonwebtoken')
const { PRIVATE_KEY, JWT_EXPIRED } = require('../utils/constant')
login(username, password).then(user => {
if (!user || user.length === 0) {
new Result('登录失败').fail(res)
} else {
const token = jwt.sign(
{ username },
PRIVATE_KEY,
{ expiresIn: JWT_EXPIRED }
)
new Result({ token }, '登录成功').success(res)
}
})
前端代码改造
utils/require.js
对响应拦截器做修改
这里的message也改成msg return Promise.reject(new Error(res.msg || 'Error'))
然后是views/login/index.vue
找到具体做登录动作的方法handleLogin
他会进入到user/login这个action
看一下这里的data 后面serToken让token保存起来了
token是保存起来了,但是运行起来报错,这都是因为/info接口还没有开发
来分析一下
进入到permission.js 全局守卫
因为去的是dashboard所以走else
之后进入到user/getInfo
又调用了一个getInfo方法
请求了后端的/user/info接口
我们还没有开发,所以后面的路都走不通了。 我们看看他的/info的框架是怎么实现的
他是直接带上token参数,我们后面要改成request里面包含token信息,拿到token信息,服务端就可以解析出用户信息,因为token信息当中有个username,我们把username解出来之后根据username查到用户信息,再返回给前端,整个实现流程是这样的状态,所以我们下面要在服务端添加一个jwt的认证,我们刚刚是生成了token,现在要来验证一下token是否在有效期范围内。
jwt认证
https://www.cnblogs.com/zkqiang/p/11810203.html
安装:npm i -S express-jwt
他的主要功能是检查所有的路由,判断当前时间是否在过期时间内,当路由中包含了没有过期的token,就可以判定为通过,
新建一个中间件router/jwt.js
这个中间件的主要用途就是做验证
这个地方卡了我好久 不是user/login而是/user/login
const expressJwt = require('express-jwt');
const { PRIVATE_KEY } = require('../utils/constant');
const jwtAuth = expressJwt({
secret: PRIVATE_KEY,
credentialsRequired: true
}).unless({
path: [
'/',
'/user/login'
],
});
module.exports = jwtAuth;
在index.js中加入这个中间件
const jwtAuth = require('./jwt')
const router = express.Router()
router.use(jwtAuth)
之后去浏览器看看
有个报错 algorithms should be set
解决:https://iseeu.blog.csdn.net/article/details/108641110
algorithms:[‘HS256’]
抛出500错误,说明拦截已经生效了 ,因为在header里面他没有找到token,所以报错了
现在去服务端做一些处理 现在发现错误以后返回的都是-1,但是我们希望token的错误会有另外的值比如-2
在constant.js 增加:CODE_TOKEN_EXPIRED:-2
当出现token错误的时候会打印出这个
老师的是这样的,他是用name标识token错误的,我这儿没有,就用code标识吧
还有,我们以前封装了Result,可以改造一下,通过Result快速生成错误信息
在Result里新建一个方法
点击登录
对前端代码再进行改造
request-相应拦截器-error
这里有个技巧 如何看到error的详细信息
这样就能弹出自定义的msg
服务端else这里的代码可以用Result改造一下
目前,会出现token验证的错误,为了解决这个错误,我们需要将header里增加一个authorization(jwt规定的)
去request的请求拦截器:
这里的user/info其实不需要带参数
开发user/info接口
在服务端部分,在数据库index.js里增加一个queryOne的方法
function queryOne(sql){
return new Promise((resolve,reject)=>{
querySql(sql).then((res)=>{
if(res&&res.length>0){
resolve(res[0])
}else{
resolve(null)
}
}).catch(err=>{
reject(err)
})
})
}
service/user.js(把login.js重命名了 变成user.js):
function findUser(username){
return queryOne(`select * from admin_user where username=${username}`)
}
router/user.js
router.get('/info', (req, res, next) => {
findUser('admin').then((user) => {
console.log(user);
if (user) {
new Result(user, '用户信息查询成功').success(res)
} else {
new Result('用户信息查询失败').fail(res)
}
})
})
因为不想让他出现password 所以查询语句要修改一下
function findUser(username){
return queryOne(`select id,username,nickname,role,avatar from admin_user where username='${username}'`)
}
因为前端获取的是roles不是role
所以在查询成功前面需要加上user.roles=[user.role]
router.get('/info', (req, res, next) => {
findUser('admin').then((user) => {
console.log(user);
if (user) {
user.roles=[user.role]
new Result(user, '用户信息查询成功').success(res)
} else {
new Result('用户信息查询失败').fail(res)
}
})
})
这样就可以进去了
继续对user/info接口进行改造 username不应该写死
username需要从token里拿,这就用到了对jwt进行解析
所以,我们要在header中拿到jwt并且对他进行解析
前端仅在 Http Header 中传入了 Token,如果通过 Token 获取 username 呢?这里就需要通过对 JWT Token 进行解析了,
需要用到jsonwebtoken里的verify方法
需要从http header里拿到authorization
在 /utils/index.js 中添加 decode 方法:
function decode(req){
const token=req.get('authorization')
return token
}
function decode(req){
let token=req.get('authorization')
if(token.indexOf('Bearer')===0){
token=token.replace('Bearer ','')
}
return jwt.verify(token,PRIVATE_KEY)
}
router.get('/info', (req, res, next) => {
const decoded=decode(req)
console.log({decoded});
if(decoded&&decoded.username){
findUser(decoded.username).then((user) => {
console.log(user);
if (user) {
user.roles=[user.role]
new Result(user, '用户信息查询成功').success(res)
} else {
new Result('用户信息查询失败').fail(res)
}
})
}else{
new Result('用户信息查询失败').fail(res)
}
})
登出
登出的时候显示接口不存在,其实登出的时候是不需要调用任何接口的
只需要将token清空,然后重定向一下
如何解决
用try-catch改造 这里不走他的接口了
logout({ commit, state, dispatch }) {
return new Promise((resolve, reject) => {
try {
commit('SET_TOKEN', '')
commit('SET_ROLES', [])
removeToken()
resetRouter()
dispatch('tagsView/delAllViews', null, { root: true })
resolve()
} catch (error) {
reject(error)
}
})
},
查询一下,发现是在NavBar里面被用到了
logout执行完以后他会跳到login页面
回顾
主要开发了两个api一个是登录api ,一个是获取用户信息api
前端请求api到服务端以后,服务端会检查jwt的白名单,在白名单的话直接调用controller,如果不在,进行jwt token的认证,是通过 jwt库 express-jwt来进行实现的,如果验证失败将返回401错误,验证成功将调用controller
在登录的时候会判断body参数,通过express-validator来进行判断,如果验证失败会调用boom.badRequest返回验证失败
之后会调用mysql数据库,写了两个方法,一个是login服务,一个是findUser服务,他们都是查询admin_User这张表去判定用户是否存在,如果是登录场景的话,会通过jwt生成一个token,将token返回给前端用户
第十三章 上传
将本地的电子书上传到服务端
现在的node服务位于本地 电子书就会上传到本地的文件夹,如果node服务部署在远端,比如说阿里云,那电子书就可以上传到阿里云服务器上
框架
新建book/components 把业务组件存放到components目录,把全局的通用组件放在src的components下,方便所有的页面进行复用。
新建book/components/Detail.vue
创建两个组件 book/create.vue book/edit.vue,因为电子书上传有两个场景,第一个是一本电子书都没有的时候去上传电子书,还有种场景是已经有电子书,去编辑电子书的时候。仍然是进入到电子书上传的页面(Detail.vue)
其中edit组件不希望他在我们的菜单中出现,因为编辑电子书肯定是要在列表当中选择一个,需要带入参数的。不想让他出现–hidden属性。还有希望高亮显示到图书列表,meta activeMenu指定高亮的路由
在edit和create组件里引入detail组件
在Detail.vue中接收一个参数 isEdit
create:
edit:
Detail.vue
sticky组件 ; 按钮:编辑的时候不需要显示帮助
给编辑/新增电子书按钮增加v-loading 默认false
新增一个样式
这个status可以放什么值呢
可以 查一下全局样式sub-navbar
这里有个细节,向sticky传入的并不是通常的class绑定,而是通过className,这是因为className其实是sticly的一个属性
然后会绑定到对应的div上
如果希望把某些元素的class交给子组件或者父组件引用的时候去使用的话就可以定义一个props 然后传入className这样一个方法
到现在sticky就实现了
接着写表单
可以用el-row来编写
表单容器分为两部分,写两个el-col
上面一部分是上传的组件,写一个el-col 可以将整个页面分成24份(占满一屏)
我们还可以写一个Warning组件,可以做一些提醒,
aside :h5的一个新的标签
https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/aside
上传组件 &token认证
src/components/EbookUpload/index.vue
在Detail.vue里引入
element ui里的组件:el-upload
点击上传
意思是需要一个接口地址
这个action不需要之前的request库,因为el-upload会自动给我们上传
之后会定义一个/book/upload的上传接口
上传的时候就会调用这个接口
报401错误 鉴权问题(之前设置了白名单是/和/login 其他接口都要带上token)
所以说上传的时候也要做token验证
增加headers (可以看el-upload文档) 可以给他写成一个计算属性,因为headers是需要动态进行计算,只要计算值没有变更的时候就不需要进行变更
在headers里要加入authorization
这样就可以完成token认证了
现在报404接口不存在的错误了
接下来就可以开发接口了
上传组件开发
:multiply表示一次是否能上传多本电子书
limit
beforeUpload 上传之前被调用 这里他的作用是调用父组件的一个事件,这个逻辑交给父组件去做,接收file参数传给父组件
成功调用事件 失败调用事件 移除事件 传入超过数量事件
:file-list 在编译的时候预先就要把fileList传入到el-upload中,这样的话上传的控件里面才能看到内容
drag show-file-list accept(接收的资源类型application/epub+zip 不是这个类型的上传不了)
:disabled 当用户上传完电子书以后就不允许他上传了,可以设为true来触发这样一个效果
err打印
加个小上传图标
这是el-upload内置的样式 会对文字做一些处理
api开发
分别是电子书文件,电子书封面,解压后的电子书
这是nginx路径,为什么要把它放到nginx路径能
这样做有个好处,就是当电子书上传到这个路径之后会自动生成一个链接
这样就可以直接通过链接来访问到资源
之后,需要使用multer库 也是一个express的中间件来开发文件上传功能
router/book.js
将所有经过book的路由都委托给bookRouter来处理
这样就可以在book文件里写嵌套路由
book.js: 调用express的Router()方法生成一个router对象 然后在对象当中调用get或post来创建请求 之后再export router就ok
上传单个文件,同时把单个文件信息放到req.file上
上传试一下,可以看到已经上传成功
上传组件功能完善
有个细节 虽然是同一本电子书但是他们名称不一样,这是multer帮我们做的事情,不然我们还要写代码改文件名字
完善onSuccess 传入两个参数 response ,file
打印一下response
这里可以拿到服务端返回的信息
拿到msg以后就可以给用户反馈上传成功
触发父组件
移除事件
超出上传数量事件
Detail.vue
fileList用途 在编辑的时候获取到电子书以后把它放到fileList 默认的时候上传组件就会有文件展示出来,
:disabled=“isEdit” 表示处于编辑状态的时候EbookUpload是点击不了不可用的
成功和移除的事件后面再写
电子书表单开发
在点击新增电子书的时候,提交的并不是ebook-upload这个组件里的内容,而是需要解析出来电子书的内容,通过这部分的内容产生表单,然后提交表单里面的内容。
这时候添加一个表单,这个表单默认的时候是空的,通过ebook-upload组件上传,然后会从服务端返回一个data对象,这个对象就是电子书对象
这个对象可以从response里面拿(暂未开发)
先把表单样式写出来
用到的知识点
el-from-item
prop属性 在表单规则校验的时候使用
MDinput组件 required 必须要输入
el-row
el-col :sapn=“12” 一半
具体代码去这里看
https://www.youbaobao.xyz/admin-docs/guide/exercise/upload.html
解决label是上下排列 还有两个label挨得紧
label-width
点击封面功能
el-tree
第十四章 电子书解析
思路:上传电子书的时候使用了multer中间件来完成上传过程,上传之后会在req产生一个file对象,这个file对象表示一个数组来代表文件的序列,file对象下包含了文件对象,文件对象里包含了文件名,文件路径,文件资源类型等等,那到这些信息之后就可以通过这些信息生成一个book对象,这里的book对象就是所说的电子书对象,然后通过book对象来完成解析的过程
models/Book.js
这里的book就代表一本电子书,他必须要给我们提供一些能力,这些能力包含从文件当中去创建对象,还有一种是在编辑的时候,需要能够根据表单的数据把它也变成一个book对象
变成book对象以后有什么好处呢?通过解析成book对象以后,就可以写一些方法比如parse方法对book对象进行解析,就可以解析到里面的一些细节信息,比如language,title ,creator等,
可以解析出电子书的目录,同时能将book对象转换成json格式(可以直接拿给前端使用),可以转换成数据库字段名快速的生成一些sql语句,所以book对象对应我们来开发整个电子书解析部分是至关重要的,所以,电子书解析很大一部分都是在编写book对象。
电子书book对象开发
传入file表示刚上传了一个电子书的文件,如果传入一个data表示更新或者插入电子书数据,data表示向数据库中插入数据,file主要用于解析电子书数据
router/book.js调用
知道file对象的内容了以后,就可以对他进行解析了
mimetype可以给他一个默认类型
我们需要给文件改个名字,因为发现返回的file.path路径是没有后缀名的,去识别这个文件的时候会有些麻烦(suffix)
生成文件的下载路径 定义url constant.js
这里改了一下UPLOAD_PATH 不用两个反斜杠了 一个‘/’也可以
这里发现了一个上古时期的bug UPLOAD_PATH后面不应该有\book的
const url=`${UPLOAD_URL}/book/${filename}${suffix}`
解压后的文件夹同理
const {MIME_TYPE_EPUB,UPLOAD_URL,UPLOAD_PATH}=require('../utils/constant')
class Book{
constructor(file,data){
if(file){
this.createBookFromFile(file)
}else{
this.createBookFromData(data)
}
}
createBookFromFile(file){
const{
destination,
filename,
mimetype=MIME_TYPE_EPUB,
path
}=file
const suffix=mimetype===MIME_TYPE_EPUB?'.epub':''
const oldBookPath=path
const bookPath=`${destination}\\${filename}${suffix}`
const url=`${UPLOAD_URL}/book/${filename}${suffix}`
const unzipPath=`${UPLOAD_PATH}\\unzip\\${filename}`
const unzipUrl=`${UPLOAD_URL}/unzip/${filename}`
}
createBookFromData(){
}
}
module.exports=Book
接下来可以创建一下电子书的解压文件夹
if(!fs.existsSync(unzipPath)){
fs.mkdirSync(unzipPath,{recursive:true})
}
接下来解压以后的文件就会丢到这个路径下面
对文件进行重命名
if(fs.existsSync(oldBookPath)){
fs.renameSync(oldBookPath,bookPath)
}
接下来根据前端所需要的一些字段定义book对象的一些属性
this.filename=filename
this.path=`/book/${filename}${suffix}`
this.filePath=this.path
this.unzipPath=`/unzip/${filename}`
this.url=url
this.title=''
this.author=''
this.publisher=''
this.contents=[]
this.cover=''
this.category=-1
this.categoryText=''
this.language=''
this.unzipUrl=unzipUrl
this.originalname=originalname
看一下结果(这里两个反斜杠的都应该改成/)
电子书解析库epub库
epubjs库是用于浏览器场景,脱离浏览器是无法工作的,因为他主要是在浏览器场景下对浏览器进行渲染
这里的epub库是在node环境下进行使用的
https://www.youbaobao.xyz/admin-docs/guide/extra/book.html#%E7%94%B5%E5%AD%90%E4%B9%A6%E8%A7%A3%E6%9E%90-2
https://github.com/julien-c/epub/blob/master/epub.js
因为需要对他的代码进行修改,所以拷贝一下集成到项目中,不是通过npm包安装的方式
utils/epub.js
安装adm-zip xml2js
Epub类提供了一个parse方法
实际去解析的时候就用到了parse方法
看一下使用方法 是使用event来实现的
传入之后,就用了一个回调的方法
后面的function是解析成功之后的回调
什么时候开始手动解析呢 需要调用epub.parse()
通过epub实例调用getChapter方法,里面再去调用一个回调
作者,标题这些信息可以去metadata里面获取
解析成功之后可以通过epub.metadata拿到
flow是整个电子书渲染的次序
getChapter获取章节(传入章节id) 获取章节对应的文本
getChapterRaw表示获得的原始文本,也就是一个html格式的文件
getImage 传入图片id 拿到图片实际的内容
getFile 传入css的id,拿到css的文件
因为存在大量的回调情况,后面会对他进行改造
电子书解析方法上
model/Book.js引入epub库
新增一个parse方法 我们给Book增加了很多属性,但是有很多都是默认值,在parse里解析,之后再给填充上
parse(){
return new Promise((resolve,reject)=>{
const bookPath=`${UPLOAD_PATH}${this.filePath}1`
if(!fs.existsSync(bookPath)){
reject (new Error('电子书不存在'))
}
})
}
测试一下
router/book.js
为了验证,把路径随便改一下
前端一直卡在这里 是因为没有返回内容,可以用到boom来快速生成异常对象
.catch(err => {
console.log("upload", err);
next(boom.badImplementation(err))
})
就是说在Book对象中使用reject包装的error 会通过路由返回给next 然后被自定义异常捕获再返回给前端,前端再进行相应的处理,这样服务端抛出的异常就可以被前端捕获到了
电子书解析方法下
看到调用parse方法之后 他会调用一个open方法
在model/Book.js:
消费
reject后
接着会到自定义异常处理
测试
把bookPath改回来
打印出来epub.metadata
需要解析出来metadata里的信息
打印book
epub对象:
containerFile :epub解析的第一个文件 根据这个文件找content.opf
rootFile:就是content.opf的位置,因为阅读电子书的时候其实就是要解析content.opf
只要能找到content.opf 那么后面的流程就好办了
manifest:资源文件 通过资源文件就可以找到封面图片
toc:目录
book对象打印
获取封面:通过epub库提供的getImage方法
这个方法需要传入两个参数,一个是id一个是callback
id就是封面图片对应的id
我们就是要把href里的图片拷贝到nginx目录下的img文件夹下 这样的话我们就可以拿到url链接,拿到这个链接就可以作为封面图片的链接
分析一下getImage方法的源码
getImage(id, callback) {
if (this.manifest[id]) {
if ((this.manifest[id]['media-type'] || "").toLowerCase().trim().substr(0, 6) != "image/") {
return callback(new Error("Invalid mime type for image"));
}
this.getFile(id, callback);
} else {
callback(new Error("File not found"));
}
};
看一下getFile如何实现的
来使用一下getImage方法 其实可以对库做一些翻新,用.then或者async await更好, 这里先用回调
这样data数据已经获取到了 (读取到了内存当中 还不在磁盘当中)
suffix根据mimetype来获取
写入文件
打卡Book.js代码
const { MIME_TYPE_EPUB, UPLOAD_URL, UPLOAD_PATH } = require('../utils/constant')
const fs = require('fs')
const Epub = require('../utils/epub')
class Book {
constructor(file, data) {
if (file) {
this.createBookFromFile(file)
} else {
this.createBookFromData(data)
}
}
createBookFromFile(file) {
console.log("createBookFromFile", file);
const {
destination,
filename,
mimetype = MIME_TYPE_EPUB,
path,
originalname
} = file
const suffix = mimetype === MIME_TYPE_EPUB ? '.epub' : ''
const oldBookPath = path
const bookPath = `${destination}/${filename}${suffix}`
const url = `${UPLOAD_URL}/book/${filename}${suffix}`
const unzipPath = `${UPLOAD_PATH}/unzip/${filename}`
const unzipUrl = `${UPLOAD_URL}/unzip/${filename}`
if (!fs.existsSync(unzipPath)) {
fs.mkdirSync(unzipPath, { recursive: true })
}
if (fs.existsSync(oldBookPath)) {
fs.renameSync(oldBookPath, bookPath)
}
this.filename = filename
this.path = `/book/${filename}${suffix}`
this.filePath = this.path
this.unzipPath = `/unzip/${filename}`
this.url = url
this.title = ''
this.author = ''
this.publisher = ''
this.contents = []
this.cover = ''
this.coverPath=''
this.category = -1
this.categoryText = ''
this.language = ''
this.unzipUrl = unzipUrl
this.originalname = originalname
}
createBookFromData() {
}
parse() {
return new Promise((resolve, reject) => {
const bookPath = `${UPLOAD_PATH}${this.filePath}`
if (!fs.existsSync(bookPath)) {
reject(new Error('电子书不存在'))
}
const epub = new Epub(bookPath)
epub.on('error', err => {
reject(err)
})
epub.on('end', err => {
if (err) {
reject(err)
} else {
const {
language,
creator,
creatorFileAs,
title,
cover,
publisher
} = epub.metadata
if (!title) {
reject(new Error('图书标记为空'))
} else {
this.title = title
this.language = language || 'en'
this.author = creator || creatorFileAs || 'unknown'
this.publisher = publisher || 'unknown'
this.rootFile = epub.rootFile
const handleGetImage = (err, file, mimetype) =>{
console.log(err, file, mimetype);
if (err) {
reject(err)
} else {
const suffix = mimetype.split('/')[1]
const coverPath = `${UPLOAD_PATH}/img/${this.filename}.${suffix}`
const coverUrl = `${UPLOAD_URL}/img/${this.filename}.${suffix}`
console.log(coverPath);
fs.writeFileSync(coverPath,file,'binary')
this.coverPath=`/img/${this.filename}.${suffix}`
this.cover=coverUrl
resolve(this)
}
}
epub.getImage(cover, handleGetImage)
}
}
})
epub.parse()
})
}
}
module.exports = Book
封面图片解析优化
有的电子书用这种方法是获取不到封面图片的
看一下这个错误出现在哪里
打印一下cover
解压出来分析一下
打开package.opf
metadata里没有标签是关于cover的 说明没有办法获得封面图片的资源id了
在manifest里找找
能看到封面的资源文件,但是是xhtml的类型,说明他是章节的内容,并不是图片 图片应该是image开头的
这个才是封面图片
他是在image路径下978开头的文件
所以封面还有一种查询方式 就是读取item下面的properties 如果为cover-image 表示是封面图片 可以把这个图片的href获取到,然后来找到它的资源文件并且从epub当中解压出来保存到本地
需要对epub.js的getImage方法改造
如果manifest没有办法从cover下获取的时候需要改进一下这里的逻辑
大致框架
如何获取coverId
const coverId=Object.keys(this.manifest).find(key=>{
this.manifest[key].properties==='cover-image'
})
getImage(id, callback) {
if (this.manifest[id]) {
if ((this.manifest[id]['media-type'] || "").toLowerCase().trim().substr(0, 6) != "image/") {
return callback(new Error("Invalid mime type for image"));
}
this.getFile(id, callback);
} else {
const coverId = Object.keys(this.manifest).find(key => (
this.manifest[key].properties === 'cover-image'
))
console.log("coverId", coverId);
if (coverId) {
this.getFile(coverId, callback)
} else {
callback(new Error("File not found"));
}
}
};
接下来开发一个比较有难度的点–解析电子书目录
在epub库并没有提供解决方案,manifest目录虽然有很多的资源文件,但是并没有形成一个顺序,我们还要确定目录的层级关系
目录解析原理和电子书解压
目录解析原理
先从spin标签下面获取toc属性 (目录的资源id)
之后在manifest里找
打开toc.ncx
navMap:导航
里面都是目录,目录可能会出现嵌套的情况
1.对电子书文件进行解压
解压后放unzip的文件夹下
通过之前getFile方法,能够直接获取到电子书文件,但是我们选择先对他进行解压,这样读取的效率会更高一些
来到自己编写的Book类中,
编写unzip方法
unzip(){
const AdmZip=require('adm-zip')
const zip=new AdmZip(Book.genPath(this.path))
zip.extractAllTo(Book.genPath(this.unzipPath),true)
}
static genPath(path){
if(!path.startsWith('/')){
path=`/${path}`
}
return `${UPLOAD_PATH}${path}`
}
}
解压出来以后就可以对他进行解析了
unzip方法是个同步方法
unzip过后就可以定义一个parseContents了 传入epub对象 因为要去toc spin里面去找toc属性
parseContents(epub){
function getNcxFilePath(){
const spine=epub&&epub.spine
console.log("spine",spine);
}
getNcxFilePath()
}
打印出spine
可以看到spine下面有个toc属性
可以找到toc对应的id/直接拿href也可以
如果没有href的时候,就找id->找manifest
parseContents(epub){
function getNcxFilePath(){
const spine=epub&&epub.spine
const manifest=epub&&epub.manifest
const ncx=spine.toc&&spine.toc.href
const id=spine.toc&&spine.toc.id
console.log("spine", spine.toc,ncx,id,manifest[id].href);
if(ncx){
return ncx
}else{
return manifest[id].href
}
}
getNcxFilePath()
}
可以发现两种方法都可以获取到目录
之后来拿到路径
const ncxFilePath=getNcxFilePath()
这样拿的是相对路径 需要把它拼成绝对路径
const ncxFilePath=Book.genPath(getNcxFilePath())
这样还是不对,需要加上unzipPath
const ncxFilePath=Book.genPath(`${this.unzipPath}/${getNcxFilePath()}`)
console.log(ncxFilePath);
还要做一件事情,判断这个路径是否存在,如果不存在需要抛出异常
if(fs.existsSync(ncxFilePath)){
}else{
throw new Error('目录对应的资源文件不存在')
}
在这里catch到
最终前端会拿到错误信息
试验一下
const ncxFilePath=Book.genPath(`${this.unzipPath}/${getNcxFilePath()+1}`)
电子书标准目录解析
打开toc.ncx
在ncx对象下面有个navMap
navMap下面每一个navPoint都是一个目录选项
navLabel:具体的目录内容
content :src 目录的路径,playOrder:目录顺序
目录可能存在嵌套,我们还需要对二级目录进行识别,所以需要有一个迭代的方法实现目录的识别(难点)
Book.js首先引用xml2js库
https://www.npmjs.com/package/xml2js
我们要取的是ncx下面的navMap属性
打印一下navMap
技巧 :看一下详细信息
字符串粘到json.cn里面
目录结构
返回的结果他给包裹在一个数组中了,如果不希望包裹在数组中,可以加个参数,
xml2js(xml,{
explicitArray:false,
ignoreAttrs:false
},function(err,result){
if(err){
reject(err)
}else{
console.log(result)
const navMap=result.ncx.navMap
console.log(JSON.stringify(navMap));
}
})
现在的结构
增加findParent方法 因为这是单级的目录,所以返回一样的数组,以后会完善
function findParent(array){
return array.map(item=>{
return item
})
}
如果是有子目录的话,是一个树状结构,树状结构是不利于前端展示的
所以需要把树状结构改成一维的结构 现在还没有这个场景,但是还是先把方法建好
navMap.navPoint=findParent(avMap.navPoint)
const newNavMap=flatten(navMap.navPoint)
newNavMap是对navMap做的一个浅拷贝
function findParent(array){
return array.map(item=>{
return item
})
}
function flatten(array){
return [].concat(...array.map(item=>{
return item
}))
}
newNavMap是复制了一份数组
epub.flow:展示顺序
epub.flow.forEach((chapter,index)=>{
if(index+1>newNavMap.length){
return
}else{
const nav=newNavMap[index]
chapter.text=`${UPLOAD_URL}/unzip/${fileName}/${chapter.href}`
console.log(chapter.text);
}
})
console.log(epub.flow);
}else{
reject('目录解析失败,目录树为0')
}
有个问题,直接用epub.flow不行吗
其实用epub.flow是有一些隐含的坑的
有的电子书是没有order和level的,不精确
所以从navMap中获取比较正宗的目录信息
在继续向chapter里面加属性
if (nav && nav.navLabel) {
chapter.label = nav.navLabel.text || ''
} else {
chapter.label = ''
}
chapter.navId=nav['$'].id
chapter.fileName=fileName
chapter.order=index+1
chapters.push(chapter)
console.log(chapter.text);
嵌套目录解析
在findParent里做一些文章
level 默认为0 下一级为1 这样返回给前端的时候就可以根据level做缩进
传入3个参数 array,level=0,pid=0
navPoint里面是没有level字段的,可以给他加个level字段
function findParent(array, level = 0, pid = '') {
return array.map(item => {
item.level = level
item.pid = pid
if (item.navPoint && item.navPoint.length) {
item.navPoint = findParent(item.navPoint, level + 1, item['$'].id)
} else if (item.navPoint) {
item.navPoint.level = level + 1
item.navPoint.pid = item['$'].id
}
return item
})
}
flatten方法:将navPoint数组变成一个扁平状态
配合这里
如果不变扁平那newNavMap的长度一定小于index+1(flow)
flatten方法
function flatten(array) {
return [].concat(...array.map(item => {
if(item.navPoint&&item.navPoint.length>0){
return [].concat(item,...flatten(item.navPoint))
}else if(item.navPoint){
return [].concat(item,item.navPoint)
}
return item
}))
}
resolve reject
可以把book返回给前端
new Result(book,‘上传成功’).success(res)
第十五章 电子书列表
电子书解析数据展示
EbookUpload组件里
this.emit(‘onSuccess’,response.data)
book/components/detail.vue
handleSuccess(data){
console.log(data);
},
拿到data数据以后
之后写个setData方法
setData(data) {
const {
title,
author,
publisher,
language,
rootFile,
cover,
originalName,
url,
contents,
contentsTree,
fileName,
coverPath,
filePath,
unzipPath,
} = data;
this.postForm = {
title,
author,
publisher,
language,
rootFile,
cover,
url,
originalName,
contents,
fileName,
coverPath,
filePath,
unzipPath,
};
},
重新执行
这里的filename大小写写错了,不过文件名称还是换成originalName更好点
树状目录展开
el-tree解决问题
传入data属性,data属性当中是个数组,数组中包含若干对象,每个对象对应一级的tree 里面包含label ,children
https://element.eleme.io/#/zh-CN/component/tree#tree-shu-xing-kong-jian
电子书目录不太一样,我们为了方便解析,只用了一维的数组,而范例是个嵌套的数组
我们去服务端来进行改造 改造成嵌套的 当然在前端做处理也可以
没有识别出pid时说明是一级目录
const chapterTree=[]
chapters.forEach(c=>{
c.children=[]
if(c.pid===''){
chapterTree.push(c)
}
})
const chapterTree = []
chapters.forEach(c => {
c.children = []
if (c.pid === '') {
chapterTree.push(c)
} else {
const parent = chapters.find(_ =>
_.navId === c.pid
)
parent.children.push(c)
}
})
console.log(chapterTree);
点击以后如何看章节内容
https://element.eleme.cn/#/zh-CN/component/tree#events
看一下data里都有啥
onContentClick(data){
console.log(data)
},
最重要的就是text参数
onContentClick(data){
if(data.text){
window.open(data.text)
}
},
随便点进一个章节
但是还是有些书有问题的 比如说这本,点进去和目录不一样 15-3有优化 这里就不做优化了
电子书表单验证功能开发
点击叉号清空
补充defaultForm
校验功能
看element-ui源码可以看到,会返回两个参数
submitForm(){
this.$refs.postForm.validate((valid,fileds)=>{
console.log(valid,fileds);
if(valid){
}else{
}
})
},
写个rules对象,它的key是el-form-item里面写的prop属性
可以直接在title里写required 这里用的自定义校验规则(传入function)
const validateRequire=(rule,value,callback)=>{
if(value===''||value.length==0){
callback(new Error('title不能为空'))
}else{
callback()
}
}
可以打印一下rule
改造
callback(new Error(rule.field+'必须填写'))
这里有个技巧,可以做个字段映射
写个对象
callback(new Error(fields[rule.field]+‘必须填写’))
现在想拿到这句话 然后展示在页面上
fields[Object.keys(fields)[0]][0].message
submitForm(){
this.$refs.postForm.validate((valid,fields)=>{
console.log(valid,fields);
if(valid){
}else{
console.log(fields[Object.keys(fields)[0]][0].message)
const message=fields[Object.keys(fields)[0]][0].message
this.$message({
message,type:'error'
})
}
})
},
给作者 出版社 语言 也加个prop author publisher language
增加映射
然后在rules添加规则
rules: {
title: [{ validator: validateRequire }],
author: [{ validator: validateRequire }],
language: [{ validator: validateRequire }],
publisher: [{ validator: validateRequire }],
},
新增电子书前端逻辑
浅拷贝两种方法:扩展运算符/object.assign
请求接口
src/api/book.js
Detail.vue引入
接口开发
utils/index.js 用到了这里的decode
book.js
router.post('/create', (req, res, next) => {
const decoded=decode(req)
console.log(decoded);
})
写一个book对象,这个book对象生成的逻辑可以从model/Book.js里写
数据库数据
打印一下data 一定要注意代码规范啊,该驼峰就驼峰 有几个没用驼峰后悔了
createBookFromData(data) {
this.fileName=data.filename
this.cover=data.coverPath
this.title=data.title
this.author=data.author
this.publisher=data.publisher
this.bookId=data.filename
this.language=data.language
this.rootFile=data.rootFile
this.originalName=data.originalname
this.path=data.filePath
this.filePath=data.filePath
this.unzipPath=data.unzipPath
this.coverPath=data.coverPath
this.createUser=data.username
this.createDt=new Date().getTime()
this.updateDt=new Date().getTime()
this.updateType=data.updateType===0?data.updateType:1
this.category=data.category||99
this.categoryText=data.categoryText||'自定义'
}
book对象就生成了 这样就可以通过对象生成sql语句了
新增电子书核心逻辑思路
新建service/book.js 有关数据库的操作
const Book=require('../model/Book')
function insertBook(book){
return new Promise((resolve,reject)=>{
try {
if(book instanceof Book) {
}else{
reject(new Error('添加的图书对象不合法'))
}
} catch (error) {
reject(error)
}
})
}
module.exports={insertBook}
router/book.js
router.post('/create', (req, res, next) => {
const decoded = decode(req)
console.log(req.body);
if (decoded && decoded.username) {
req.body.username = decoded.username
}
const book={}
bookService.insertBook(book).then((res)=>{
console.log(res);
}).catch((err)=>{
next(boom.badImplementation(err))
})
console.log(book)
})
这就是大致框架了
判断电子书是否存在,如果已经存在,就移除掉 需要把数据库里的信息移除掉,还要把文件移除掉
insert以后还要把目录插入到目录表里
操作数据库会存在大量的异步操作,用到async await 变成同步方法 减少回调的次数,如果用promise的话会存在大量嵌套
逻辑就是这样,接下来写具体的方法
数据库操作
insert方法判断第一个参数传入的是否为一个对象
判断是否为对象的方法:utils/index.js
function isObject(o){
return Object.prototype.toString.call(o)==='[object Object]'
}
用这个做判断非常精确
function insert(model,tableName){
return new Promise((resolve,reject)=>{
if(!isObject(model)){
reject(new Error('插入数据库失败,插入数据非对象'))
}
})
}
来实验一下
回到insert方法
数据库操作有个技巧
keys.push(`\`${key}\``)
为什么呢 比如说select from from book 其中第一个from并不是关键字而是个key,但是数据库会自动识别出他是个关键字就会报错,加了反引号之后就没有这个问题了
if(keys.length>0&&values.length>0){
let sql=`insert into \`${tableName}\`(`
const keysString=keys.join(',')
const valuesString=values.join(',')
sql=`${sql}${keysString}) values (${valuesString})`
console.log(sql)
}
接着尝试执行sql语句能否成功
if(keys.length>0&&values.length>0){
let sql=`insert into \`${tableName}\`(`
const keysString=keys.join(',')
const valuesString=values.join(',')
sql=`${sql}${keysString}) values (${valuesString})`
console.log(sql)
const conn=connect()
try {
conn.query(sql,(err,result)=>{
if(err){
reject(err)
}else{
resolve(result)
}
})
} catch (error) {
reject(error)
}finally{
conn.end()
}
}else{
reject(new Error('对象中没有任何属性'))
}
提示数据库没有path这个字段
这里推荐个做法
Book.js新增个方法toDb 对book对象进行过滤而不是整体都拿去使用
把path去掉
toDb() {
return {
fileName: this.fileName,
cover: this.cover,
title: this.title,
author: this.author,
publisher: this.publisher,
bookId: this.bookId,
language: this.language,
rootFile: this.rootFile,
originalName: this.originalName,
filePath: this.filePath,
unzipPath: this.unzipPath,
coverPath: this.coverPath,
createUser: this.createUser,
createDt:this.createDt,
updateDt: this.updateDt,
updateType:this.updateType,
category: this.category,
categoryText: this.categoryText
}
}
这个book就要改成book.toDd() 把toDb返回的结果传给insertBook 这样会报错,因为insertBook里面会判断传入的对象是不是Book的实例
写在这里是合适的
这样的话点击添加就可以发现数据库多了条数据
前端交互优化
insertBook(book).then((res)=>{}) 这里不能写res因为success方法会自动传入这里的res 但是实际上应该传入上面红箭头所指的res
前端:
拿到后端的返回值,显示到前端页面
这次不用$message了
https://element.eleme.cn/#/zh-CN/component/notification#notification-tong-zhi
用一个跟他很像的 $notify
还有个细节,上传成功之后可以把列表数据都移除掉 remove方法里用过
this.postForm = Object.assign({}, defaultForm);
列表是移除了 但是还是有bug
这里写个setDefault方法,来解决这些问题
把这个置空书名就没了
setDefault(){
this.postForm = Object.assign({}, defaultForm);
this.fileList=[]
},
接下来要把校验结果消除
setDefault(){
this.fileList=[]
this.$refs.postForm.resetFields()
},
这是因为没有传入prop,如果没有传入prop,他就不认为是一个表单的选项
目录还没有消除
setDefault() {
this.contentsTree = [];
this.fileList = [];
this.$refs.postForm.resetFields();
},
移除也是调用这个方法
添加目录到数据库功能
接下来写insertContents方法
需要取到book下的contents
但是前面把contents删掉了 所以传进来没有目录
前端:
后端接收:
可以编写一个getContents
确保能拿到contents之后就可以做插入数据库操作了
对比一下还是有很多冗余字段
可以建一个对象一次赋值,这里有个技巧—通过lodash实现
通过lodash可以使用它的方法来实现我们想要的功能
调用insert方法
await db.insert(_content,‘contents’)
电子书删除功能
下面两个逻辑都写完了 接下来写移除的逻辑
首先把exists函数写了 功能是判断电子书是否存在
为了方便测试 把前端的this.setDefault();注释掉
这种情况就需要把当前上传的电子书给移除
function removeBook(book) {
if(book){
book.reset()
}
}
进入Book.js
static pathExists(path){
if(path.startsWith(UPLOAD_PATH)){
return fs.existsSync(path)
}else{
return fs.existsSync(Book.genPath(path))
}
}
来模拟一下
单个文件删除:unlinkSync
解压路径删除记得加上recursive:true fs.rmdirSync(Book.genPath(this.unzipPath),{recursive:true})
这样就只保留一个文件了
老师这里又加了个删除数据库的操作,不知道为啥要加这一步,应该没有写入数据库才对,所以我觉得不用加这个删除数据库的逻辑了
电子书查询api
实现编辑功能首先要拿到fileName,根据filename到数据库查询 查询目录和内容
查到之后返给前端 所以需要在路由中接收个参数
之后可以在Detail.vue里拿到动态路由参数
思路
现在要实现的就是getBook的api
src/api/book.js
注意get方法使用的是params,如果是post则使用data
服务端:
router.book.js
大致框架
getBook方法在service/book.js里写
完善getBook逻辑
需要到book表和contents表进行查询
进一步处理让数据展示出来
resolve(book[0])
但是标题,封面,目录都没有
先解决封面的问题
来解决目录的显示问题
现在的contents是个数组的结构,需要将数组转换成一个contentsTree
其实前面我们已经做过这个逻辑了,
const chapterTree = []
chapters.forEach(c => {
c.children = []
if (c.pid === '') {
chapterTree.push(c)
} else {
const parent = chapters.find(_ =>
_.navId === c.pid
)
parent.children.push(c)
}
})
这时候目录还是出不来 前端代码改一下
接下来还有个问题没解决 就是文件的信息
给fileList赋值就ok
编辑电子书
接下来写这里的逻辑
前端增加个接口
虽然还没开发出来接口 但是还是可以去network里面看一下 可以发现接口和参数都能正确发出去了
现在服务端增加接口
和之前create的接口很相似(写错了 应该是update)
现在去service/book.js里写updateBook的逻辑
前端
这里的book.fileName应该要加引号的
数据库操作文件写一个update方法
第十六章 电子书编辑和删除功能开发
电子书列表
知识点 directive waves用法 v-waves
pagination用它封装好的组件 这些都是可以传入的参数
<template>
<div class="app-container">
<!-- 查询条件 -->
<div class="filter-container">
<!-- filter -->
<el-input
v-model="listQuery.title"
placeholder="书名"
size="normal"
clearable
style="width: 200px"
class="filter-item"
@keyup.enter.native="handleFilter"
@clear="handleFilter"
@blur="handleFilter"
></el-input>
<el-input
v-model="listQuery.author"
placeholder="作者"
size="normal"
clearable
style="width: 200px"
class="filter-item"
@keyup.enter.native="handleFilter"
@clear="handleFilter"
@blur="handleFilter"
></el-input>
<el-select
v-model="listQuery.category"
placeholder="分类"
clearable
class="filter-item"
@change="handleFilter"
@clear="handleFilter"
>
<el-option
v-for="item in categoryList"
:key="item.value"
:label="item.label"
:value="item.value"
>
</el-option>
</el-select>
<el-button
type="primary"
class="filter-item"
size="default"
icon="el-icon-search"
style="margin-left: 10px"
@click="handleFilter"
>
点击查询
</el-button>
<el-button
type="primary"
class="filter-item"
size="default"
icon="el-icon-search"
style="margin-left: 10px"
@click="handleCreate"
>
点击新增
</el-button>
<el-checkbox
v-model="showCover"
label=""
:indeterminate="false"
class="filter-item"
style="margin-left: 10px"
@change="changeShowCover"
>
显示封面
</el-checkbox>
</div>
<!-- 表格组件 -->
<el-table></el-table>
<!-- 翻页 -->
<Pagination :total="0" />
</div>
</template>
<script>
import Pagination from "../../components/Pagination/index";
import { getCategory } from "../../api/book";
// 这个老是报错,应该是依赖的问题,使用方法:v-waves
// import waves from '../../components/directive/waves'
export default {
components: { Pagination },
data() {
return {
listQuery: {},
showCover: false,
// 查询条件是动态的
categoryList: [],
};
},
mounted() {
this.getCategoryList();
},
methods: {
getCategoryList() {
getCategory().then((response) => {
this.categoryList = response.data;
});
},
changeShowCover(value) {
this.showCover = value;
console.log(this.showCover);
},
handleFilter() {
console.log("handleFilter", this.listQuery);
},
handleCreate() {
// 页面切换到/book/create 切换到上传图书
this.$router.push("/book/create");
},
},
};
</script>
<style lang="scss" scoped>
</style>
水波效果
api
拿到category以后就去请求,请求以后赋值,之后显示到页面
写label两种方式
图书分类api
打开category视图
有个疑问
这里return的categoryList能在then里面接收到吗,我的认知里面应该是要return一个promise对象的
我知道了,async里面return的对象会自动帮我们转换成promise
图书列表样式
https://element.eleme.io/#/zh-CN/component/table#ji-chu-biao-ge
https://element.eleme.io/#/zh-CN/component/table#table-column-attributes
https://element.eleme.io/#/zh-CN/component/table#table-column-attributes
el-table
sort-change事件排序
<el-table
:key="tableKey"
v-loading="listLoading"
:data="list"
border
fit
highlight-current-row
style="width: 100%"
@sort-change="sortChange"
>
<el-table-column
prop="id"
label="ID"
sortable="custom"
align="center"
width="80"
>
</el-table-column>
<el-table-column label="书名" sortable="custom" align="center" width="80">
<template slot-scope="{ row: { title } }">
<span>{{ title }}</span>
</template>
</el-table-column>
</el-table>
接下来拿数据
虽然还没写接口逻辑,但是可以先验证一下query数据有没有传进去
来尝试一下
可以看到已经不报404了 而且后端能收到数据了 这样流程就串起来了
有个问题,author和title应该采取模糊查询
有个难点,不确定category,author,title是否传入
前端收到数据
<template>
<div class="app-container">
<!-- 查询条件 -->
<div class="filter-container">
<!-- filter -->
<el-input
v-model="listQuery.title"
placeholder="书名"
size="normal"
clearable
style="width: 200px"
class="filter-item"
@keyup.enter.native="handleFilter"
@clear="handleFilter"
@blur="handleFilter"
></el-input>
<el-input
v-model="listQuery.author"
placeholder="作者"
size="normal"
clearable
style="width: 200px"
class="filter-item"
@keyup.enter.native="handleFilter"
@clear="handleFilter"
@blur="handleFilter"
></el-input>
<el-select
v-model="listQuery.category"
placeholder="分类"
clearable
class="filter-item"
@change="handleFilter"
@clear="handleFilter"
>
<el-option
v-for="item in categoryList"
:key="item.value"
:label="item.label + '(' + item.num + ')'"
:value="item.value"
>
</el-option>
</el-select>
<el-button
type="primary"
class="filter-item"
size="default"
icon="el-icon-search"
style="margin-left: 10px"
@click="handleFilter"
>
点击查询
</el-button>
<el-button
type="primary"
class="filter-item"
size="default"
icon="el-icon-search"
style="margin-left: 10px"
@click="handleCreate"
>
点击新增
</el-button>
<el-checkbox
v-model="showCover"
label=""
:indeterminate="false"
class="filter-item"
style="margin-left: 10px"
@change="changeShowCover"
>
显示封面
</el-checkbox>
</div>
<!-- 表格组件 -->
<el-table
:key="tableKey"
v-loading="listLoading"
:data="list"
border
fit
highlight-current-row
style="width: 100%"
@sort-change="sortChange"
>
<el-table-column
prop="id"
label="ID"
sortable="custom"
align="center"
width="80"
>
</el-table-column>
<el-table-column
label="书名"
sortable="custom"
align="center"
width="180"
>
<template slot-scope="{ row: { title } }">
<span>{{ title }}</span>
</template>
</el-table-column>
<el-table-column
label="作者"
sortable="custom"
align="center"
width="100"
>
<template slot-scope="{ row: { author } }">
<span>{{ author }}</span>
</template>
</el-table-column>
<!-- 换种方式 ,不使用插槽 -->
<el-table-column
label="出版社"
prop="publisher"
sortable="custom"
align="center"
width="150"
>
</el-table-column>
<el-table-column
label="分类"
prop="categoryText"
sortable="custom"
align="center"
width="150"
>
</el-table-column>
<el-table-column
label="语言"
prop="language"
sortable="custom"
align="center"
width="80"
>
</el-table-column>
<el-table-column
v-if="showCover"
label="封面"
prop="cover"
align="center"
width="150"
>
<template slot-scope="{ row: { cover } }">
<a :href="cover" target="_blank">
<img :src="cover" style="width: 120px; height: 180px" />
</a>
</template>
</el-table-column>
<!-- <el-table-column v-if="showCover" label="封面" prop="cover" align="center" width="150">
<template slot-scope="scope">
<a :href="scope.row.cover" target="_blank">
<img :src="scope.row.cover" style="width:120px;height:180px">
</a>
</template>
</el-table-column> -->
<el-table-column
label="文件名"
prop="fileName"
sortable="custom"
align="center"
width="150"
>
</el-table-column>
<el-table-column
label="文件路径"
prop="filePath"
sortable="custom"
align="center"
width="150"
>
</el-table-column>
</el-table>
<!-- 翻页 -->
<Pagination :total="0" />
</div>
</template>
<script>
import Pagination from "../../components/Pagination/index";
import { getCategory, listBook } from "../../api/book";
// 这个老是报错,应该是依赖的问题,使用方法:v-waves
// import waves from '../../components/directive/waves'
export default {
components: { Pagination },
data() {
return {
// 存在多个table的时候能够对table进行区分
tableKey: 0,
listLoading: true,
listQuery: {},
showCover: false,
// 查询条件是动态的
categoryList: [],
// 表格数据源
list: [],
};
},
mounted() {
this.getCategoryList();
this.getList();
},
methods: {
getList() {
this.listLoading = true;
listBook(this.listQuery).then((response) => {
console.log(response);
const { list } = response.data;
this.list = list;
this.listLoading = false;
});
},
// 排序事件
sortChange(data) {
console.log("sortChange", data);
},
getCategoryList() {
getCategory().then((response) => {
this.categoryList = response.data;
});
},
changeShowCover(value) {
this.showCover = value;
console.log(this.showCover);
},
handleFilter() {
console.log("handleFilter", this.listQuery);
this.getList();
},
handleCreate() {
// 页面切换到/book/create 切换到上传图书
this.$router.push("/book/create");
},
},
};
</script>
<style lang="scss" scoped>
</style>
因为写了fit 所以无法左右横移
经常采用的方法:–fixed
使用前:
使用后
<el-table-column label="操作" align="center" width="120">
<template slot-scope="{ row }">
<el-button
type="text"
size="default"
@click="handleUpdate(row)"
icon="el-icon-edit"
></el-button>
</template>
</el-table-column>
携带分页参数
分页和查询
本来pageSize应该是20的,因为我把老的数据都删掉了,所以数据比较少,pageSize写小点测试一下
接下来加入查询条件
这里的操作很妙
标题的模糊查询
模糊查询高亮
wrapperKeyword(k,v){
function highlight(value){
return `<span style='color:#18900f'>${value}</span>`
}
if(!this.listQuery[k]){
return v
}else{
return v.replace(new RegExp(this.listQuery[k]),v=>highlight(v))
}
},
getList() {
this.listLoading = true;
listBook(this.listQuery).then((response) => {
console.log(response);
const { list } = response.data;
this.list = list;
this.listLoading = false;
this.list.forEach(book=>{
book.titleWrapper=this.wrapperKeyword('title',book.title)
book.authorWrapper=this.wrapperKeyword('author',book.author)
})
});
},
bug
bug 一个书名无法匹配多个 只能匹配第一个 还有一个不区分大小写
解决办法就是在正则表达式加一些参数
排序&分页优化
取出prop,order
我们设desc为-id asc为+id
点击以后需要再次发请求
服务端
解出sort之后
分页功能
先查询book表里有多少电子书
返回值:一个数组
返回给前端
看到返回的是字符串
修改
这几个参数返回给前端主要是要修改pagination这个组件
但是点击翻页按钮并没有变化,而且切换每页显示的数量也没有变化
这样翻页功能和改变每页的数量的功能就实现了
接下来有个显示图片的bug
过滤器优化表格字段显示
现在假如说有一本文件路径是空的
增加个上传时间
parseTime utils下面自带的方法
删除功能
拿到点击的fileName 通过fileName来进行后续的操作
服务端
优化 完结
刷新url的时候原来的查询条件全部丢失
方案:把参数保存到url中
实现这个功能需要进行一些改造
在created调用了parseQuery
在parseQuery里面不仅要对page,pageSize,sort进行封装,还要增加一步:获取到一个新的对象(this.route.query)
之后和当前的query做一个合并
if(query){
query.page&&(query.page = +query.page)
query.pageSize&&(query.pageSize = +query.pageSize)
}
getList就不适用了
点击查询
点击下面的翻页并没有更新
解决:
但是又有问题了 就是鼠标移开以后不会更新
虽然push了 但是路由并没有更新
这是一个难点 虽然路由更新了,但是页面并没有刷新
需要通过路由钩子来解决,监听两次路由的变化
现在的问题就是更新完了以后列表没有刷新
如何比较两个对象相不相等呢 变成json字符串来对比
这样就ok了
还有个问题
category还原的方法有很多,比较推荐的是
服务端:
还有个问题,先选到第二页,在输入查询条件(只有一页)这时候会出现这种情况
所以每次查询的时候都要讲页数置为1
现在是修排序的状态bug
浏览器地址栏回车以后虽然能正确排序,但是那个箭头没有变颜色
有个地方写错了 default-sort应该写在el-table下面
–
前端代码为什么要打包
因为前端的浏览器中只能识别出js html css 现在我们的代码包含的是.vue文件
这是浏览器无法识别的
还有在代码中充斥着大量的import 和export语法,这是模块化语法 import使用的是 es module规范 es6的模块化
在服务端中使用的是类似module.export 是commonjs语法
这些语法在浏览器是无法运行的
打包的目的是让代码再浏览器中运行
打包是要通过webpack进行打包
命令行输入npm run build:prod
打包完毕后 会生成一个dist目录
index.html就是入口文件,也是平常所说的单应用,因为他是单个页面的应用
他可以实现整个应用就只有一个页面index.html
里面所有的内容都是组件,通过js来进行动态的界面替换
这样打包完了以后 我们的项目就完成了,完成了以后尝试运行一下这个index.html
直接在浏览器打开
发现会报错
造成这个问题的原因是什么呢
因为这里写的是绝对路径
资源文件的路径设置怎么做呢
在重新打包一下
去index.html文件看一下
发现已经可以看到界面了
但是点击登录仍然登录不上去
没有关系 我们接着往下走
之后打开8089端口
发现是可以登进去的
目前nginx服务器现在是在本地,需要把它挪到线上
云服务器设密码
因为阿里云现在安全级别比较高
点击配置安全组规则
ssh采用的是22端口,所以应该是可以访问的
所以不是这个问题
应该重启一下
因为输入who没有出来东西
所以exit退出从新登了一下
这次可以了
这两句话可以实现免密登录(可忽略)
ssh-keygan生成rsa的秘钥
然后~/.ssh/id_rsa.pub找到文件
后面再加上远程连接的内容 root@47.103.29.206
更简便的是对host文件进行修改
这个意思是长时间不用的话连接会被服务器断开
解决办法:找到这个文件
这个代表每30秒客户端就会保持与服务端的连接 就可以避免自动断电的问题了
改完了他不会立即生效,需要重启一下
然后登出
在登录看看会不会自动断开
nodejs环境搭建
看看有没有生效
nvm并没有生成
vim .bash_profile
可以看到并没有生成nvm
应该是在bashrc下面
我这儿一直显示找不到nvm
后来又重新安了一遍 然后仔细看他的提示
这样跟着他输一遍就好了
然后输入nvm 显示出一大段东西的时候就代表安装好了
之后nvm install node
在通过node安装最新版本
这时候npm可以用了 npm -v
现在可以来安装cnpm了
cnpm的包就会安装到nvm下面
which cnpm
nginx安装
yum:是centos自带的 通过yum来安装centos的依赖
yum -y install pcre*
这些库都是要安装nginx的时候需要的依赖
再来安装openssl的依赖
yum -y install openssl*
之后来下载nginx
可以先建一个目录 叫nginx
mkdir nginx 待会就将源码放进去
找到nginx下载地址
我们选择源码编译形式来进行安装
因为nginx是用C语言来写的,所以要用make指令来对他进行编译 编译完了以后,使用make install来进行安装,
我们还得看看make这些指令存不存在,如果不存在还得通过yum来进行安装
用安装make 和gcc
也不知道这是安好了还是没安好
然后就可以来解压了
进入来看一下 这就是源码目录
有个绿色的configure 代表是个可执行文件
centos执行文件使用./的方式来执行的, 或者用sh 来执行
然后用make -j4来进行编译
编译完之后 make install来进行安装
安装完之后
可以看到nginx已经生成了
但是输入nginx 显示没找到,现在还没法直接使用
怎么办呢
进入到uer/bin目录下
cd /usr/bin
ll一下 usr/bin目录下的指令都是可以直接访问的
这里可以直接做个软连接
ln -s /usr/local/nginx/sbin/nginx nginx
可以理解为创建了一个快捷方式
可以看到他指向了哪
cd 回到root
这时候就可以调用nginx nginx就启动了
可以输入ps -ef|grep nginx看一下进程 进程启动了
可以通过nginx -s stop来停止nginx服务
来看一下nginx配置文件
进入conf
打开这个
可以看到默认启动的是80端口
路径是根路径下的html文件夹
打开看一下
现在通过nginx指令将服务启动起来
然后通过阿里云的ip来访问
但是我的并不能访问
试一下这个https://blog.csdn.net/qq_21882763/article/details/113823460 失败
https://www.cnblogs.com/songanwei/p/9239821.html
https://bbs.csdn.net/topics/396990545 这里有个评论 curl localhost失败
我知道了
需要添加一个80端口
这样就能访问了
接下来做一些个性化的定义
希望nginx能支持我们默认的配置
进入nginx目录下,创建一个nginx.conf
然后打开nginx的config文件
输入a进入编辑模式 把user改为root
然后到文件的最后添加一些内容
这是我们刚刚建的那个文件
意思是将root/nginx下面所有带有.conf的配置文件融合到当前的配置文件当中
接着打开刚刚建的nginx.conf
写入一些东西
因为我们监听的端口是80端口,和主配置文件有冲突,所以把主配置文件的端口改一下
给他改成9000
接着继续在nginx.conf写配置
接着进入upload 创建一个index.html
vim index.html
写点东西 这里写了个h1标签 里面写了hello world
检查一下配置文件
nginx -t
出错了
打开这个配置文件
发现并不能显示行号
那么如何显示行号呢 :set nu
是因为include那句话少了个分号,所以报错了
还是有错
额 又忘加分号了
重启nginx nginx -s reload
okkkk
现在就可以把我们的文件上传到上面了
如何实现呢
可以通过ftp工具 fileZilla / xftp 等很多软件
下载fileZilla 记得选client 而不是server
添加新站点
之后把我们最先打包好的文件拖到upload里面
在实际生产环境中建议大家不要开启index 重新修改nginx.conf配置文件
把这个关闭
不知道下面文件路径的话是没有办法访问到这边的文件的
yum install git 为了下载网易云接口
clone github项目
进入到项目
node app.js 但是退出了以后就
pm2 让它永远活着
写个脚本也可以
代码build好直接上传到服务器是可以的
但是如果我们新push了一些内容,还需要手动编译,编译完了以后打开ftp工具把代码上传上去手动上传上去
了解一下自动化部署
安装
因为nginx是用C语言来写的,所以要用make指令来对他进行编译 编译完了以后,使用make install来进行安装,
我们还得看看make这些指令存不存在,如果不存在还得通过yum来进行安装
用安装make 和gcc
也不知道这是安好了还是没安好
然后就可以来解压了
进入来看一下 这就是源码目录
有个绿色的configure 代表是个可执行文件
centos执行文件使用./的方式来执行的, 或者用sh 来执行
然后用make -j4来进行编译
编译完之后 make install来进行安装
安装完之后
可以看到nginx已经生成了
但是输入nginx 显示没找到,现在还没法直接使用
怎么办呢
进入到uer/bin目录下
cd /usr/bin
ll一下 usr/bin目录下的指令都是可以直接访问的
这里可以直接做个软连接
ln -s /usr/local/nginx/sbin/nginx nginx
可以理解为创建了一个快捷方式
可以看到他指向了哪
cd 回到root
这时候就可以调用nginx nginx就启动了
可以输入ps -ef|grep nginx看一下进程 进程启动了
可以通过nginx -s stop来停止nginx服务
来看一下nginx配置文件
进入conf
打开这个
可以看到默认启动的是80端口
路径是根路径下的html文件夹
打开看一下
现在通过nginx指令将服务启动起来
然后通过阿里云的ip来访问
但是我的并不能访问
试一下这个https://blog.csdn.net/qq_21882763/article/details/113823460 失败
https://www.cnblogs.com/songanwei/p/9239821.html
https://bbs.csdn.net/topics/396990545 这里有个评论 curl localhost失败
我知道了
需要添加一个80端口
这样就能访问了
接下来做一些个性化的定义
希望nginx能支持我们默认的配置
进入nginx目录下,创建一个nginx.conf
然后打开nginx的config文件
输入a进入编辑模式 把user改为root
然后到文件的最后添加一些内容
这是我们刚刚建的那个文件
意思是将root/nginx下面所有带有.conf的配置文件融合到当前的配置文件当中
接着打开刚刚建的nginx.conf
写入一些东西
因为我们监听的端口是80端口,和主配置文件有冲突,所以把主配置文件的端口改一下
给他改成9000
接着继续在nginx.conf写配置
接着进入upload 创建一个index.html
vim index.html
写点东西 这里写了个h1标签 里面写了hello world
检查一下配置文件
nginx -t
出错了
打开这个配置文件
发现并不能显示行号
那么如何显示行号呢 :set nu
是因为include那句话少了个分号,所以报错了
还是有错
额 又忘加分号了
重启nginx nginx -s reload
okkkk
现在就可以把我们的文件上传到上面了
如何实现呢
可以通过ftp工具 fileZilla / xftp 等很多软件
下载fileZilla 记得选client 而不是server
添加新站点
之后把我们最先打包好的文件拖到upload里面
在实际生产环境中建议大家不要开启index 重新修改nginx.conf配置文件
把这个关闭
不知道下面文件路径的话是没有办法访问到这边的文件的
yum install git 为了下载网易云接口
clone github项目
进入到项目
node app.js 但是退出了以后就
pm2 让它永远活着
写个脚本也可以
代码build好直接上传到服务器是可以的
但是如果我们新push了一些内容,还需要手动编译,编译完了以后打开ftp工具把代码上传上去手动上传上去
了解一下自动化部署
|