⭐ 笔记更新

新增图片懒加载 2021.11.22

本笔记图片数量已经超过100张,为了减少流量消耗,使用 lazy-load

新增弹窗拓展 2021.11.21

😜 Vue 学习笔记 1

环境配置与安装

Node.js + Vue3.0 + Vsc 环境搭建: https://www.runoob.com/vue3/vue3-install.html

Vsc插件 https://xiaonenglife.oss-cn-hangzhou.aliyuncs.com/Notes/Vue/VSC%20Plugins.png

理论知识

JS、Jquery、模板引擎、Vue

原生js需要解决各个浏览器的代码执行兼容性问题,所以有了Jquery,但是Jquery会造成频繁操作DOM元素,前端模板引擎提供方法,就可以很方便地生成DOM元素,缺点举例 Pasted image 20211117224453.png

"表格排序的情况下会重新渲染不必要的行",导致效率不高(如图排序,仅修改2,3两行,但前端模板引擎提供的方法会将所有表格都重新进行渲染,效率低)。Angular.js和Vue.js能够帮助我们减少不必要的DOM操作,提高渲染效率。Vue还提供了双向数据绑定的概念(通过框架提供的指令,我们前端程序员只需要关心数据的业务逻辑,不再关心DOM是如何渲染的了。)

业务逻辑占了程序70%-80%的代码量。

学习VUE的好处:增强自己就业时候的竞争力。(人无我有,人有我优)

(你平时不忙的时候,都在干嘛?)

什么是框架

框架是一套完整的解决方案,对项目的入侵性较大(项目如果需要更换框架,需要重新架构整个项目)

什么是库

库(插件):提供某一个小功能,对项目的侵入性较小,如果某个库无法完成某些需求,可以很容易切换到其他库实现需求。

⭐ MVC 与 MVVM 的区别

MVC

MVC是后端分层开发的概念:M是Model层,是数据库中的数据,主要做的是数据的CRUD(增加(Create)、检索(Retrieve)、更新(Update)和删除(Delete))。V是视图层,一般看做前端的页面,C是路由分发层与业务逻辑层。

MVVM

MVVM是前端视图层的分层思想,主要把每个页面,分成了M、V和VM。其中,==VM是MVVM思想的核心==;因为VM是M和V之间的的调度者。M保存的是每个页面中单独的数据,V就是每个页面中的HTML结构,VM它是一个调度者,分割了M和V,每当V层想要获取后保存数据的时候,都要由VM做中间处理。前端页面中使用MVVM思想,主要是为了让我们开发更佳方便,因为MVVM提供了数据的双向绑定,双向绑定是由VM提供的。 Pasted image 20211117233230.png

MVVM是Model-View-ViewModel的缩写。MVVM是一种设计思想。Model 层代表数据模型,也可以在Model中定义数据修改和操作的业务逻辑;View 代表UI 组件,它负责将数据模型转化成UI 展现出来,ViewModel 是一个同步View 和 Model的对象。在MVVM架构下,View 和 Model 之间并没有直接的联系,而是通过ViewModel进行交互,Model 和 ViewModel 之间的交互是双向的, 因此View 数据的变化会同步到Model中,而Model 数据的变化也会立即反应到View 上。ViewModel 通过双向数据绑定把 View 层和 Model 层连接了起来,而View 和 Model 之间的同步工作完全是自动的,无需人为干涉==因此开发者只需关注业务逻辑,不需要手动操作DOM, 不需要关注数据状态的同步问题,复杂的数据状态维护完全由 MVVM 来统一管理==

区别

mvvm和mvc区别?它和其它框架(jquery)的区别是什么?哪些场景适合?

mvc和mvvm其实区别并不大。都是一种设计思想。主要就是mvc中Controller演变成mvvm中的viewModel。mvvm主要解决了mvc中大量的DOM 操作使页面渲染性能降低,加载速度变慢,影响用户体验。区别:vue数据驱动,通过数据来显示视图层而不是节点操作。场景:数据操作比较多的场景,更加便捷。

更多资料:https://www.cnblogs.com/dingdc/p/13468613.html

代码实操

简单导入包并创建Vue应用

<head>
    <!-- 1、导入Vue的包 -->
    <script src="https://unpkg.com/vue@next"></script>
</head>
<body>
    <div id="root"></div>
    <div id="root1"></div>
    <script>
    Vue.createApp({  //创建一个Vue实例
        template:'<div>hello world!</div>'  //模板,在元素里面用这个模板作为展示的内容
    }).mount('#root'); //在root的元素上去使用Vue
    </script>
</body>


<body>
    <div id="root"></div>
    <script>
    Vue.createApp({  //创建一个Vue实例
        data() {
            return {
                content:1
            }
        },
        template:'<div>{{content}}</div>'  //模板,在元素里面用这个模板作为展示的内容
    }).mount('#root'); //在root的元素上去使用Vue
    //找到root节点,把template上的东西放到root节点下
    //因为template里的内容content是变量,data里面定义了一个content,所以变量是1
    //{{}} 表示的是变量,我们在data里面定义了一个content内容,我们在template里面就可以用里面的内容
    </script>
</body>

    Vue.createApp({  //创建一个Vue实例
        data() {
            return {
                content:1
            }
        },
        mounted() {  //当你页面加载完成,就会执行这个函数
            console.log('mounted');
            setInterval(()=>{  //这是定时器,会让content每秒钟加1,以前我们要手动操作DOM,现在不需要
                this.content+=1;  //this.content 指向的是 data 下面的 content
                //等价于 this.$data.content +=1;
            },1000);
        },
        template:'<div>{{content}}</div>'  //模板,在元素里面用这个模板作为展示的内容
    }).mount('#root'); //在root的元素上去使用Vue

问题:模块中是反引号 ` 而不是单引号 ' Pasted image 20211117225405.png Pasted image 20211117225409.png Pasted image 20211117225411.png

解决方案:模板是包在 ECMAScript 2015 反引号 (`) 中的一个多行字符串。 反引号 (`) — 注意,不是单引号 (\') — 允许把一个字符串写在多行上, 使 HTML 模板更容易阅读。

参考文章:template多行编写的方式_松门一枝花-CSDN博客

反转字符串

<body>
    <div id="root"></div>
    <script>
    const app = Vue.createApp({
        data() {
            return {
                content:'hello world'
            }
        },
        methods: {
            handle(){
                this.content = this.content.split('').reverse().join('');
            }
        },
        template:`
            <div>
            {{content}}
            <button @click="handle">反转</button>  //@click 是 v-on:click 的简写
            </div>
            `
    }).mount('#root');
    </script>

Pasted image 20211117225509.png

v-if

const app = Vue.createApp({

  data() {
            return {
                show:true,
                content:'hello world'
            }
        },
        methods: {
            handle(){
                this.show = !this.show
            }
        },
        template:`
            <div>
            <span v-if="show">Hello World</span>  
            <button @click="handle">显示 隐藏</button>
            //v-if 指的是span标签的存在与否 
            </div>
            `
    }).mount('#root');

Pasted image 20211117225613.png

v-for 双向绑定

        const app = Vue.createApp({
            data() {
                return {
                    inputValue:'',
                    list: []
                }
            },methods: {
                handle(){
                    this.list.push(this.inputValue);  //面向数据进行编程
                    this.inputValue = ''; //因为双向绑定,model层修改view层
                }
            },template:`
        <div>
            <input v-model="inputValue"/>  //双向绑定 &nbsp;&nbsp;
            <button @click="handle">增加</button>
            <ul>
                <li v-for="(item,index) in list">{{item}} {{index}} test</li>
            </ul>
        </div>
            `
        }).mount('#root');

Pasted image 20211117225719.png Pasted image 20211117225722.png

v-bind 学习

template:`
        <div>
            <input v-model="inputValue"/>  //双向绑定 &nbsp;&nbsp;
            <button @click="handle" :title="inputValue" >增加</button> //:title 是 v-bind 简写
            <ul>
                <li v-for="(item,index) in list">{{item}} {{index}} test</li>
            </ul>
        </div>
            `

组件思想

什么是组件

你不可能把所有东西都写在一个页面里,可以把页面拆成各个组件组成起来。 (组件最小是DOM标签)当标签变多时,维护成本会变大。 Pasted image 20211117230503.png

组件化

Pasted image 20211117230544.png

const app = Vue.createApp... //用这个实例app名字是为了注册组件
...  
   });
app.component('todo-item',{  //将每一个列表项封装成组件
    template:`<div>hello world</div>`
});
app.mount('#root');

尝试组件化

const app = Vue.createApp({ //用这个实例app名字是为了注册组件
    data() {
        return {
            inputValue:'',
            list: []
        }
    },methods: {
        handle(){
            this.list.push(this.inputValue);  //面向数据进行编程
            this.inputValue = ''; //因为双向绑定,model层修改view层
        }
    },template:`
<div>
    <input v-model="inputValue"/> 
    <button @click="handle" :title="inputValue" >增加</button>
    <ul>
        <todo-item v-for="(item,index) in list"  <!-- ! 循环list中的每一项的内容放到item,下标放到index--!>
        :content="item"  <!-- ! 往todo-item组件上面挂载第一个属性 content 值是上面的item --!>
        :index="index"    <!-- ! 再挂载第二个属性值index,值是上面index--!>
        />   <!-- !给组件传入一个属性,用的v-bind方法 --!>
    </ul>
</div>
    `
});
app.component('todo-item',{  //将每一个列表项封装成组件
    props:['content','index'],  //组件获取属性,接收挂载的属性的内容
    template:`<li>{{index}}--{{content}}</li>` //逻辑
});
app.mount('#root');

代码分析


<div id="root"></div>
<script>
    // createApp 表示创建一个Vue应用,存储到app变量中
    // 传入的参数表示,这个应用最外层的组件,应该如何展示
    // 即传入的参数决定了最外层的组件长什么样子,显示什么内容
    // mvvm 设计模式,m代表model数据,v代表view视图,vm代表viewmodel数据视图连接层
    // vue 组件帮我们完成了 vm 过程
    const app = Vue.createApp({
        data() {
            return {
                message:'hello world'
            }
        },
        template:"<div>{{message}}</div>"
    }); //创建一个Vue的应用,需要传递参数进去
    const vm = app.mount('#root'); //这个Vue应用只作用于root节点
    // vm 代表的就是 vue 应用的根组件
</script>
数据和视图的一些关联关系是由vm来维护的,我们可以通过 vm.$data. 获取根节点的数据

Pasted image 20211117230737.png Pasted image 20211117230739.png

⭐ 理解生命周期函数

生命周期函数:在某一时刻会自动执行的函数

  • 当写完 Vue.createApp 和后面的 mount,首先进行代码逻辑分析,分析完后依次执行
  • beforeCreate
  • 初始化,分析数据和模板之间绑定的相关内容
  • Create
  • 判断实例里是否存在template选项,如果存在,将template变成render函数,如果不存在,将root对象的innerHTML编译成函数
  • beforeMount
  • 函数与数据结合生成新的DOM,去替换我们页面上的root的div标签,替换掉之后展示的是vue实例想要创建的内容,即已经挂载
  • mounted
  • beforeUpdate
  • Updated
  • beforeUnmount
  • unmounted
// 生命周期函数:在某一时刻会自动执行的函数
const app = Vue.createApp({
    data() {
        return {
            message:'hello world'
        }
    },
    //在实例生成之前会自动执行的函数
    beforeCreate() {
        console.log("beforeCreate");
    },
    //在实例生成之后会自动执行的函数
    created() {
        console.log("created");
    },
    //在模板已经被编译成函数之后(在组件内容被渲染到页面之前)立即自动执行的函数
    beforeMount() {
        console.log(document.getElementById('root').innerHTML,"beforeMount");
    },
    //在组件内容被渲染到页面之后自动执行的函数
    mounted() {
        console.log(document.getElementById('root').innerHTML,"mounted");
    },
    //当data中的数据发生变化时会自动执行的函数
    beforeUpdate() {
        console.log(document.getElementById('root').innerHTML,"beforeUpdate");
    },
    //当data中的数据发生变化,页面重新渲染后,会自动执行的函数
    updated() {
        console.log(document.getElementById('root').innerHTML,"updated");
    },
    //当vue应用失效时,自动执行的函数
    beforeUnmount() {
        console.log(document.getElementById('root').innerHTML,"beforeUnmount");
    },
    //当vue应用失效时,且dom已经销毁,自动执行的函数
    unmounted() {
        console.log(document.getElementById('root').innerHTML,"unmounted");
    },
    template:"<div>{{message}}</div>" //可以不写,将内容写到HTML中
});
const vm = app.mount('#root'); 

常用模板语法

const app = Vue.createApp({
    data() {
        return {
            message:'<strong>hello world</strong>'
        }
    },
    template:`<div v-html="message"></div>` // v-html 通过 html 的方式展示 message 的变量,就不会做转义
});
const vm = app.mount('#root'); 

const app = Vue.createApp({
    data() {
        return {
            disable:true,
            show:false,
            message:'<strong>hello world</strong>'
        }
    },
    template:`<div v-html="message" :title="message"></div>
        <div><input :disabled="disable" /></div>
        {{'a'+'b'}}
        {{Math.max(1,2)}}
        <div v-once>{{message}}</div>
        <div v-if="show">你好呀</div>
    ` 
    // v-html 通过 html 的方式展示 message 的变量,就不会做转义
    // v-bind 使标签属性内容与message绑定
    // {{}} 允许js表达式
    // 但不能写 if()xxxx  这样是语句了
    // v-once 只渲染一次,再修改数据不改变(降低无用渲染,提高渲染性能)
    // v-if 判断标签展不展示
});
const vm = app.mount('#root'); 

const app = Vue.createApp({
    data() {
        return {
            disable:true,
            show:false,
            name:'filename',
            event:'mouseenter',
            message:'<strong>hello world</strong>'
        }
    },
    methods: {
        handle(){
            //e.preventDefault();
            alert(123);
        }
    },
    template:`<div 
    @[event]="handle" 
    v-html="message" 
    :title="message"
    :[name]="message"
    >
    </div>
    <form action="https://www.baidu.com" @click.prevent="handle">
            <button type="submit">提交</button>
    <form/>
    ` 
});
const vm = app.mount('#root'); 
//简写 v-on -> @  v-bind -> :
// []动态属性 v-bind , 事件 也可以用动态属性
// prevent 为修饰符用于阻止默认行为

😜 Vue 学习笔记 2

VSC

VSC中快速移动光标的方法:

Home 移到行首
End 移到行尾
Ctrl+Home 移到最前面
Ctrl+End 移到最后面 
Alt+Up 将某一行上移
Alt+Down 将某一行下移
Shift+Alt+Up/Down 向上向下复制该行
Ctrl+Shift+K 删除该行
Ctrl+Enter 下一行行首
Ctrl+Shift+Enter 上一行行首
Ctrl+K+S 查看所有快捷键

ctrl+f 搜索到所有匹配,alt+enter 选中所有匹配,ctrl+l 选中匹配行,delete 删除

Ctrl+G 跳转到指定行

数据、方法、计算属性、侦听器

const app = Vue.createApp({
    data() {
        return {
            message: "hello world",
            count: 2,
            price: 5,
            newTotal : 10
        }
    },
    watch: {
        //计算属性不能做异步操作,监听器可以监听变量的改变执行相关指令
        //price发生变化时,函数会执行
        price(current,prev){ //可以获得两个值,一个是current,一个是prev
            setTimeout(()=> {
                console.log("price changed");
            },3000)
            console.log(current,prev);
            this.newTotal = current * this.count;
        }
    },
    computed: {
        //计算属性,里面有个total属性,用于计算
        //我这个数据是其他几个数据算出来的,当其他几个数据变化时,total也会重新计算
        total() {
            return this.count * this.price;
        },
        date() {
            return Date.now() + this.count;
        }
    },
    methods: {
        handle() {
            console.log("click", this.message); // this指向的vue的实例       
        }
        //为什么不写成箭头函数 handle: ()=> { } 这样写this是外层的this,如果找不到,就找到最外层,指向window
        ,
        formatString(string) {
            return string.toUpperCase();
        },
        getTotal() {
            return this.count * this.price;
        },
        getDate() {
            return Date.now();
        }
    },
    template: `
   <div @click="handle">{{formatString(message)}}</div> 
   <div>{{total}}</div>
   <div>{{getTotal()}}</div>
   <div>{{date}}</div>
   <div>{{getDate()}}</div>
   <div>{{newTotal}}</div>
   `
    //插值表达式里也可以用方法
    //computed计算属性里的total与methods里的getTotal()差异是什么
    //当计算属性依赖的数据发生变更时才会重新执行计算
    //方法是只要页面重新渲染才会重新计算

    //computed 和 method 都能实现的功能,建议使用 computed,因为有缓存
    //computed 和 watch 都能实现的功能,建议使用 computed 因为更加简洁
});
const vm = app.mount('#root');
//message是对象返回的第一层的数据,可以直接 vm.message
//输入 vm.count = 2,页面上的值被改变

Pasted image 20211027020246.png

总结

插值表达式里可以用methods规定的方法
methods中写函数不能使用箭头函数
computed是计算属性,当其他数据变化时,规定的属性也会变化
methods里规定计算函数,当页面重新渲染时,就会执行
watch用于监听数据的变化,当监听的对象发生改变时,执行规定的操作,优点是能进行异步操作

computed 与 methods、watch

当它们都能实现计算返回值的功能时

优先考虑 computed
因为 computed 比 methods 更高效,有缓存
因为 computed 比 watch 更简洁(watch还需要在data里申明一个变量)


样式绑定语法

const app = Vue.createApp({  //父组件,主动调用子组件
    data() {
    return {
        classString:'red',
        classObject:{ red: false, green: true },
        classArray: ['red','green', {brown : false} ],
        styleString:'color : yellow;', //不建议
        styleObject:{  //建议
            color: 'orange',
            background: 'yellow'
        }
    }
},
    template:`
    <div :class="classString">hello world
        <demo class="green"/>    
    </div>

    <div :style="styleObject">hello world 
    </div>
    `
});

app.component('demo',{  //子组件
    template:
    `
    <div :class="$attrs.class">one</div>
    <div>two</div>
    `
})

const vm = app.mount('#root');

Pasted image 20211027023015.png

总结

样式绑定的几种方式

① 直接在标签里写入class或style属性
② data域:使用字符串、对象、数组表示

如果有子组件,如果只有一个标签,可以直接修改其class属性或者在父组件中为子组件标签添加class样式

但是如果子组件最外层下有多个标签(根节点),那么父组件添加的class样式将不起作用,因为不知道要添加给哪个标签,这时候可以给需要添加的标签写上 :class="$attrs.class"

style建议使用对象表示,字符串可读性低


条件渲染

const app = Vue.createApp({  //父组件,主动调用子组件
    data() {
    return {
        show:false, //如果是false的时候,dom直接移除了,v-if是控制dom的存在与否
        conditionOne:false,
        conditionTwo:true
    }
},
    template:`
    <div v-if="show">
        控制DOM的存在与否
    </div>

    <div v-if="conditionOne">if</div>
    <div v-else-if="conditionTwo">elseif</div>
    <div v-else>else</div>

    <div v-show="show">
        控制标签的Display属性
    </div>

    `
});
// 要频繁显示隐藏DOM元素,就用v-show,因为不会频繁销毁,性能会好些
// 不涉及频繁销毁创建,v-if和v-show都差不多

const vm = app.mount('#root');

Pasted image 20211027025637.png

总结

v-if v-show 之间的区别

v-if 控制DOM的存在与否
v-show 控制标签的display属性 需要多次显示隐藏元素优先使用

条件判断语法

v-if
v-else-if
v-else

列表循环渲染

const app = Vue.createApp({
 data() {
    return {
        listArray: ['dell','lee','teacher'],
        listObject:{
            firstName:'dell',
            lastName:'lee',
            job:'teacher'
        }
    }
},
methods: {
    handle(){
        //数组
        //1. 使用数组的变更函数 
        //this.listArray.push('hello');  
        //this.listArray.pop(); //删最后一个
        //this.listArray.shift(); //删第一个
        //this.listArray.unshift('hello');  //往开头加
        //this.listArray.splice();
        //this.listArray.reverse();
        //vue的底层为了优化性能,会尽量发现以前能够复用的DOM元素
        //但有些情况下Vue不知道能不能复用,在做V-for指令时加一个key值属性
        //不改变原始数组的引用

        //2. 直接替换数组
        //this.listArray = ['bye','world']
        //this.listArray = ['bye'].concat(['world']);
        //this.listArray = ['bye','world'].filter(item => item === 'bye');
        //会改变原始数组的引用

        //3. 直接更新数组的内容
        //this.listArray[1] = 'hello';
        //不改变袁术数组的引用

        //对象
        //直接添加对象的内容,也可以自动的展示出来
        //this.listObject.age = 100;
        //this.listObject.sex = 'male';
        //this.listObject['xxx'] = 100;
    }
},
    template:`
    <div v-for="(item,index) in listArray" :key="index">
            {{item}} {{index}}
    </div>
    <br>
    <template v-for="(value,key,index) in listObject" >
        <div v-if="key !== 'lastName'">
            {{value}} {{key}} {{index}}
        </div>
    </template>
    <br>
    <div v-for="item in 10">
            {{item}} 
    </div>
    <button @click="handle">新增</button>
    `
});
// ! 当你做vue做循环的时候,一定要给标签加一个key值,这样性能会得到提升
// 在template里v-for行写 v-if="key !== 'lastName'" 既做循环又做if,循环优先级大于if优先级,if不生效
// 循环和判断在一个标签上会有问题,所以在里面包一层,但是会有一个隐藏的问题,外层多加了一个div标签
// 为了解决这个问题使用 template 占位符的语法,实际上不会做渲染
const vm = app.mount('#root');

Pasted image 20211027035221.png

总结

使用语句 v-for 来进行循环

可循环的数据有:数组、对象、纯数字

如何使用循环,在一个标签内添加v-for
循环数组 v-for="(item,index) in xxx"
循环对象 v-for="(value,key,index) in xxx"
循环数字 v-for="(item) in xxx"

修改数组的方法 (变更函数,替换数组,修改数组)
① push、pop、shift、unshift、splice、reverse、sort 变更函数
② 替换数组 [...].concat([]) 、[...].filter(item => item \=\=\= 'xxx') 、[...]
③ 修改数组 [...].[1] = 'xxx'

修改对象的方法 (直接修改对象的内容)
① object.xxx = ' '
② object['xxx'] = ' '

可能出现的问题
① Vue虽然会自动搜索复用DOM元素,但在循环的时候还是需要我们给定循环标签一个Key值,让Vue在下一次循环时判断与复用,提高性能
② 有时候我们需要在循环的时候要进行判断,不能把判断与循环写在一起,原因是循环的优先级大于判断,因此在内部我们还要再加一层标签进行判断。但是这样会出现一个小问题,会导致外层多了一层标签。因此我们把外层循环的标签改为template,即占位符,不做渲染


事件绑定

const app = Vue.createApp({
 data() {
    return {
        counter: 0
    }
},
methods: {
    handle(num,event){
        //console.log(event);
        this.counter+=num;
    },
    handle2(){
        console.log(123);
    },
    handle3(){
        this.counter++;
    },
    handle4(){
        alert('div alert');
    },
    handleKey(){
        //我们默认写法是用获取event判断,但在Vue中我们有按键修饰符
        console.log('keydown');
    },
    handleClick(){
        console.log('mouse');
    }
},
    template:`
  <div>{{counter}}</div>
  <div>
  <button @click="handle(2,$event), handle2()">点击 1 2</button>
  </div>
  <div @click="handle4">
      <button @click="handle3">点击 4 3</button>
  </div>
  <div @click="handle4">
    <button @click.stop="handle3">点击 4 3 + stop</button>
    </div>
  <div @click.self="handle4">
    {{counter}}
    <button @click="handle3">点击 4 3 + self</button>
    </div>
    <div>
        <input @keydown.enter="handleKey" />
    </div>
    <div>
        <div @click.middle="handleClick" >123</div>
    </div>
    <div>
        <div @click.ctrl.exact="handleClick" >456</div>
    </div>
    `
});
// 我们可以在绑定事件的时候直接写表达式,但是能写的内容非常少,不建议这样写
// 使用 $event 获取原生事件
// 如果绑定一个事件想要执行多个函数的时候,要用逗号间隔,并且不能直接写函数的引用,要添加括号

// 当你点击button的时候,实际上事件是冒泡的,会冒泡到上层的div上,所以上层也会感知到点击事件
// 所以我们使用 修饰符 stop 来停止向外做事件的冒泡

// 修饰符 self 要求点击的是 当前元素,如上述代码中的div元素,而点击按钮则不起效果
// 当你点button的时候,你触发的是button而不是div,它会判断这个事件是不是自己触发的
// 而点counter的时候,counter在div标签里,是自己会被触发

// 修饰符 prevent 阻止默认修饰符

// 修饰符 capture 把事件运营模式变成捕获,从外到内

// 修饰符 once 绑定只执行一次

// 修饰符 passive 提升滚动性能 scroll.passive

// 按键修饰符
// keydown.enter、delete、esc、up、down、left、right 等

// 鼠标修饰符
// left, right, middle

// 精确修饰符
// exact , 如上述代码案例,必须精确的按住了ctrl键再点击后才可以触发,有BUG!
const vm = app.mount('#root');

Pasted image 20211027044732.png

总结

如果函数没有参数,可添加一个event参数 获取原生事件
如果函数有其他参数,需要先在模板里添加 $event 获取原生事件

事件修饰符
我们可以在模板绑定事件的时候直接写表达式,但是能写的内容过少,不建议这样写
当绑定多个函数的时候,我们用逗号间隔,并为每个函数后面添加 ( )

当外层标签内层标签绑定事件相同时,当触发内层事件,由于事件会冒泡,外层会感知到并且触发,所以内层加个事件修饰符 stop 来制止

当只想这个事件对自己的标签生效时,使用 self 修饰符

当只想这个事件的函数执行一次的时候,使用 once 修饰符

其他修饰符有 passive,prevent , capture

按键修饰符
原来我们需要捕获event事件来做判断,但是Vue有按键修饰符
举例 keydown.enter, esc, left, right, up, down, delete

鼠标修饰符
click.left, middle, right

精确修饰符(有bug)
exact

😜 Vue 学习笔记 3

VSC KeyBoards

Home 移到行首
End 移到行尾
Ctrl+Home 移到最前面
Ctrl+End 移到最后面 
Alt+Up 将某一行上移
Alt+Down 将某一行下移
Shift+Alt+Up/Down 向上向下复制该行
Ctrl+Shift+K 删除该行
Ctrl+Enter 下一行行首
Ctrl+Shift+Enter 上一行行首
Ctrl+K+S 查看所有快捷键

ctrl+f 搜索到所有匹配,alt+enter 选中所有匹配,ctrl+l 选中匹配行,delete 删除

Ctrl+G 跳转到指定行

Ctrl+D 选中当前单词

选中一个变量,按F2进行变量重命名

按住Alt,用鼠标左键点击,可以出现多个光标,输入的代码可以在光标处同时增加
按住Ctrl + Alt,再按键盘上向上或者向下的键,可以使一列上出现多个光标
选中一段文字,按shift+alt+i,可以在每行末尾出现光标
光标放在一个地方,按ctrl+shift+L或者ctrl+f2,可以在页面中出现这个词的不同地方都出现光标
按shift+alt,再使用鼠标拖动,也可以出现竖直的列光标,同时可以选中多列何光标操作,可以按Ctrl + U取消


表单中的双向绑定

// input, textarea, checkbox, radio, select
// 修饰符 lazy, number, trim
const app = Vue.createApp({
    data() {
        return {
            message: 'hello',
            check: [],
            radio: '',
            select:[],
            options:[
                {text:'A', value: {value:'A'}},
                {text:'B', value: {value:'B'}},
                {text:'C', value: {value:'C'}}   //也可以存对象
            ],
            check2:"对",
            message2:'123'
        }
    },
    template: `
<div>
    {{message}}<input v-model="message" />    
</div>
<br>
<div>
    {{message}}<textarea v-model="message"></textarea>
</div>
<br>
<div>
    {{check}}
    jack<input type="checkbox" v-model="check" value="jack"/>
    dell<input type="checkbox" v-model="check" value="dell"/>
    li<input type="checkbox" v-model="check" value="lee"/>
</div>
<br>
<div>
    {{radio}}
    jack<input type="radio" v-model="radio" value="jack"/>
    dell<input type="radio" v-model="radio" value="dell"/>
    li<input type="radio" v-model="radio" value="lee"/>
</div>
<br>
<div>
    {{select}}
    <select v-model="select" multiple>
        <option disabled value=''>请选择内容</option>
        <option v-for="item in options" :value="item.value">{{item.text}}</option>
    </select>
</div>
<br>
<div>
    {{check2}}
    <input type="checkbox" v-model="check2" true-value="对" false-value="否"/>
    选中的时候不显示true显示其他内容
    </div>
<br>
<div>
    {{message}}<input v-model.lazy="message" /> 不让反应那么快,当input发生变化时,先不变化,当失去焦点的时候再同步变化   
</div>
<br>
<div>
    {{typeof(message2)}}<input type="number" v-model="message2" />  老版本需要用 number修饰符,新版本解决了这个问题
</div>
<br>
<div>
    {{message}}<input v-model.trim="message" />  去除最左边和做右边的空格 
</div>
`});
// v-model 双向绑定方法
// 什么是双向绑定,数据变了,控件里的值会改变;控件的值变了,数据的值也会改变
// 如果input框用于双向绑定,就不必写value的东西了
// 
// checkbox加上 value ,再把绑定的数据类型改成数组,勾选时数组添加对应的内容
//
// radio只能单选,推荐数据为字符串,不用数组
//
// select 可以用数组对象循环输出option,并且value里不仅可以存放字符串也可以存放对象
// 
// checkbox 可以添加 true-value 和 false-value 将绑定数据类型改为字符串,可以不显示true跟false
const vm = app.mount('#root');

Pasted image 20211028053150.png

总结

什么是双向绑定?

数据变了,控件里的值会改变;控件的值变了,数据的值也会改变
- 表单
- input
- checkbox 一般用数组表示,而且要加上value属性,输出值可以自定义
- radio 单选,一般用字符串表示
- select 可以单选与多选,看情况而定,选项可以用数组对象循环输出
  • 修饰符
    • lazy 当触发blur事件时才同步,即失去焦点,可能会带来性能提升
      • number 修饰符
    • trim 去除数据两边的空格

组件定义、复用性、局部组件、全局组件

 const app = Vue.createApp({});
 //这是创建了一个vue实例(应用),会接受一个参数,用来决定根组件怎么渲染
// counter 变量是局部组件,在vue里感知不到局部组件的存在,不像全局组件一样会挂到APP上去
const Counter = {
    data() {
        return {
            count: 1
        }
    },
    template: `
    <div @click="count++">{{count}} 组件是可以被复用的,且相互间独立,数据是当前组件独享的</div>
    `
};

const HelloWorld = {  //为什么驼峰式去命名,因为js语法不支持-作为变量的名字,但又和普通变量没区别,所以要对首字母大写
    template:`<div>hello world</div>`
};

// 组件的定义
// 组件具备复用性
// 全局组件,只要定义了,处处可以使用,使用简单,性能不高(会一直挂载到app上)
// 局部组件,定义了,要注册之后才能使用,性能比较高,使用起来有些麻烦,建议大写字母开头,驼峰命名
// 局部组件使用时要进行注册,要做一个名字和组件间的映射对象,你不写映射,Vue底层也会帮你自动映射好
const app = Vue.createApp({
    //我现在创建一个Vue应用,会使用一部分局部组件,有一个名字叫dell的组件用的是局部组件counter
    components: {   
        counter:Counter,  //'dell': counter  //两种写法
        'hello-world':HelloWorld
        //如果不写显式映射,vue也会自动帮我们完成,我们只需要把用到的局部组件写上去就行
    },
    template: `
     <div>
        <counter-parent/>
        <counter/>
        <dell/>
        <hello-world/>
        <counter/> 
    </div>
    `
});

// app.component 定义的是全局组件,不仅在父组件可以用,在其他组件也可以用
// 但是对性能有一定损耗,你不用它,它也在,挂载在app上
//  app.component('counter-parent',{
//     template:'<counter />'
// })

// app.component('counter',{
//     data() {
//         return {
//             count:1
//         }
//     },
//     template:`
//     <div @click="count++">{{count}} 组件是可以被复用的,且相互间独立,数据是当前组件独享的</div>
//     `
// });

const vm = app.mount('#root');

总结

Pasted image 20211028053214.png

组件是怎么被定义的?

Pasted image 20211028014334.png

将一个页面拆成多个部分,以组成层次来进行分组

最上面的是根组件,根组件与子组件建立关系,整个页面维护成本降低

简单说说组件的复用性?

一个组件能被多次使用,组件里的数据是由这个组件所独享的,不共享


  • 怎么建立全局组件 :使用app.component

    • 优点:不仅可以在父组件上使用,在其他组件上也可以用
    • 缺点:无论有没有使用组件,会一直挂载在app上,对性能有损耗
  • 怎么建立局部组件:使用const xxx = { } ,需要在app里使用components进行注册(不写显式注册Vue底层也会帮我们完成)

    • 优点:局部组件要注册了之后才能使用,性能较高
    • 缺点:正是因为要注册,使用起来比较麻烦
    • 注意:最好对局部组件采用首字母大写+驼峰命名法的形式

组件间传值及传值校验

const app = Vue.createApp({
    //动态传参来解决类型不一致的问题
    data() {
        return {
            num: 1234
        }
    },
    template: `<div>
        <test content1="123" :content2="num"/> 静态传参与动态传参
    </div>`
});

app.component('test', {
    // 子组件 props
    // 不校验:['content']
    // Type: String, Boolean, Array, Object, Function, Symbol, Number
    // required 必填
    // default 默认值
    // validator
    props: {
        content1: String, 
        content2: {
            type: Number,
            required: false,
            validator: function(value){  //不写箭头函数防止this指向window
                return value<1000;
            },
            default: 789 // 这里也可以用函数
        }
    },
    //['content1','content2'],  //接受父组件调用子组件时传递的值(父组件调用子组件的标签,通过标签上的属性向子组件传递值)
    // 子组件使用props接受传递过来的值
    // 子组件可以对父组件传来的值进行校验,将数组改成对象
    template: `<div>{{typeof(content1)}} {{typeof(content2)}} {{content2}}</div>`
});

const vm = app.mount('#root');

Pasted image 20211028053232.png

总结

父子组件传值
父组件调用子组件的标签,通过标签上的属性向子组件传递值,子组件通过props获取父组件传递过来的值

静态传值与动态传值
静态传值传递字符串,动态传值用v-bind更加灵活类型更多

子组件接受父组件传递过来的内容时可以做校验
Type : String, Boolean, Number, Function, Symbol, Array, Object
Required : 父组件必须传入一个参数
Validator :方法体进行校验,返回一个Boolean
Default : 默认值


单项数据流的理解

// v-bind = "params"
// 单项数据流的概率:子组件可以使用父组件传递过来的数据,但是绝对不能修改传递过来的数据
const app = Vue.createApp({
    data() {
        return {
            params: {
                num: 1234,
                a: 123,
                b: 456,
                c: 789
            },
            num: 1
        }
    },
    template: `<div>
        <test :num="params.num" :a="params.a" :b="params.b"/>
        <test :="params"/> 传递很多参数的时候考虑这种语法
        <counter :count="num"/>
    </div>`
    //<test :content="num" :a="a" :b="b" :c="c"/>  ==  <test :="params"/> (将data传入的值)放进对象里 

    //定义很长的东西的时候,父组件传递用的属性建议用 - 如 data-abc 不要写 dataAbc 因为html会自动转换成小写
    //子组件接收的时候用驼峰式的语法接收, dataAbc  (特殊的语法)
    // 即属性传的时候,使用content-abc 这种命名,接的时候,使用contentAbc 命名
});

app.component('test', {
    props: ['num', 'a', 'b', 'c'],
    template: `<div> {{num}} {{a}} {{b}} {{c}}</div>`
});

//父组件可以传递给子组件数据,但是在子组件里不能反向修改父组件的数据,只能用父组件的数据
app.component('counter',{
    props:['count'],
    data() {
        return {
            myNum: this.count //相当于复制了一份内容,作为自己的一部分,修改自己的
        }
    },
    template:`<div @click="myNum++">{{myNum}}</div>`
});

const vm = app.mount('#root');

总结

当父组件有大量参数传递给子组件时,不妨将这些参数全部合并为一个对象,在父组件中使用v-bind一次性传递给子组件,不用写过多的 :xxx=" "语句

假设父组件传入的参数属性值较长时,用 - 间隔,子组件接收的时候用驼峰式的语法接收, 即属性传的时候,使用content-abc 这种命名,接的时候,使用contentAbc 命名

单项数据流的概念
子组件可以使用父组件传递过来的数据,但是绝对不能修改传递过来的数据

解决办法:

子组件data里复制了一份内容,作为自己的一部分,修改自己的
Pasted image 20211028053112.png


Non Props 是什么

//Non Props 属性
//父组件给子组件传值的时候,子组件不用props接收
//底层会把父组件传来的内容放在子组件最外层的dom标签下

//假如不希望子组件最外层的DOM上展示msg  inheritAttrs:false

//Non Props做样式修饰,class有用

//假如子组件有多个同级标签,那么需要指定传给谁,:="$attrs" 全部 :msg="$attrs.msg" 某一个
const app = Vue.createApp({
    template: `<div>
        <counter msg="hello" style="color:red"/>
    </div>`
});

app.component('counter',{
    //props:['msg'],
    //inheritAttrs:false,
    mounted() {
        console.log(this.$attrs);
    },
    template:`
    <div :="$attrs">counter</div>
    <div :msg="$attrs.msg">counter</div>
    <div :aaaa="$attrs.msg">counter</div>
    <div>counter</div>                   
    `
});

const vm = app.mount('#root');

Pasted image 20211028055737.png

总结

什么是Non Props?

父组件给子组件传入参数,但是子组件没有使用props接收参数。那么传入的参数会添加到子组件最外层的DOM元素

用途:用于添加样式或修改Class

如果不希望子组件最外层的DOM展示传入的参数,使用 inheritAttrs:false
# inheritAttrs: false使用
假如子组件同级有多层DOM,需要自己规定哪些DOM是需要展示的


  • :="$attrs"

  • :msg="$attrs.xxx"



父子组件之间的通信

const app = Vue.createApp({
    data() {
        return {
            count:1
        }
    },
    // methods: {
    //     handleAdd(num){
    //         this.count=num;
    //     }
    // },
    template: `<div>
        <counter v-model="count" />
    </div>`
    // :modelValue="count" @add="handleAdd"/> 
    //触发事件的时候用驼峰,监听事件的时候用 - 
});

app.component('counter',{
    props:['modelValue'], //名字必须是modelValue,如果需要换参数名字,在父组件 v-model后面加 :xxx
    //emits:['add'] ,
    /*
    {
        add: (count)=>{if(count>0)return true;else return false;} //报警告,校验参数函数
    }, //让你看到这个emits,因为会有很多事件触发,逐行看代码麻烦,提高维护性
    */
    methods: {
        handleItemClick(){
            this.$emit('update:modelValue',this.modelValue+3);  //触发事件的名字也是固定的
        }
    },
    template:`
    <div @click="handleItemClick">counter {{modelValue}}</div>                   
    `
});

const vm = app.mount('#root');

总结

父子之间通信有如下几个情况,父组件传一个参数给子组件,父组件传多个参数给子组件。子组件同步修改父组件的值(使用 this.$emit)

一个参数,子组件修改父组件

普通方法

const app = Vue.createApp({
    data() {
        return {
            num:1
        }
    },
    methods: {
        add(num){
            this.num = num;
        }
    },
    template: `<div>
        <counter :count="num" @add="add" />
    </div>
    `
});

app.component('counter',{
    props:['count'],
    methods: {
        handleClick(){
            this.$emit('add',this.count+3);
        }
    },
    template:
    `
    <div @click="handleClick">{{count}}</div>
    `
});

const vm = app.mount('#root');

v-model

const app = Vue.createApp({
    data() {
        return {
            num:1
        }
    },
    template: `<div>
        <counter v-model="num"/>
    </div>
    `
});

app.component('counter',{
    props:['modelValue'],
    methods: {
        handleClick(){
            this.$emit('update:modelValue',this.modelValue+3);
        }
    },
    template:
    `
    <div @click="handleClick">{{modelValue}}</div>
    `
});

const vm = app.mount('#root');

多个参数子组件修改父组件

        const app = Vue.createApp({
            data() {
                return {
                    count:1,
                    count1: 1,
                    count2: 'a'
                }
            },
            // methods: {
            //     handleCountOne(num){
            //         this.count1 = num;
            //     }
            // },
            template: `<div>
                <counter v-model:count="count" v-model:count1="count1" v-model:count2="count2"/>
            </div>`
        });

        app.component('counter',{
            props:['count','count1','count2'], 
            methods: {
                handleItemClick(){
                    this.$emit('update:count',this.count+3);  
                },
                handleItemClick1(){
                    this.$emit('update:count1',this.count1+3);  
                },
                handleItemClick2(){
                    this.$emit('update:count2',this.count2.toUpperCase());  
                }
            },
            template:`
            <div @click="handleItemClick">counter {{count}}</div>     
            <div @click="handleItemClick1">counter {{count1}}</div>    
            <div @click="handleItemClick2">counter {{count2}}</div>                
            `
        });

        const vm = app.mount('#root');

传入一个参数和修饰符做多余操作

const app = Vue.createApp({
    data() {
        return {
            str:'abcde'
        }
    },
    template: `<div>
        <counter v-model.uppercase.xxx="str"/>
    </div>
    `
});

app.component('counter',{
    props:{
    modelValue : String,
    modelModifiers : {
        default: ()=> ({})
    }
    },
    methods: {
        handleClick(){
            console.log(this.modelModifiers);
            let s = this.modelValue;
            if(this.modelModifiers.uppercase)s = s.toUpperCase();
            this.$emit('update:modelValue',s+'p');
        }
    },
    template:
    `
    <div @click="handleClick">{{modelValue}}</div>
    `
});

const vm = app.mount('#root');

😜 Vue 学习笔记 4

VSC KeyBoards

Home 移到行首
End 移到行尾
Ctrl+Home 移到最前面
Ctrl+End 移到最后面 
Alt+Up 将某一行上移
Alt+Down 将某一行下移
Shift+Alt+Up/Down 向上向下复制该行
Ctrl+Shift+K 删除该行
Ctrl+Enter 下一行行首
Ctrl+Shift+Enter 上一行行首
Ctrl+K+S 查看所有快捷键

ctrl+f 搜索到所有匹配,alt+enter 选中所有匹配,ctrl+l 选中匹配行,delete 删除

Ctrl+G 跳转到指定行

Ctrl+D 选中当前单词

选中一个变量,按F2进行变量重命名

按住Alt,用鼠标左键点击,可以出现多个光标,输入的代码可以在光标处同时增加
按住Ctrl + Alt,再按键盘上向上或者向下的键,可以使一列上出现多个光标
选中一段文字,按shift+alt+i,可以在每行末尾出现光标
光标放在一个地方,按ctrl+shift+L或者ctrl+f2,可以在页面中出现这个词的不同地方都出现光标
按shift+alt,再使用鼠标拖动,也可以出现竖直的列光标,同时可以选中多列何光标操作,可以按Ctrl + U取消


插槽和具名插槽解决组件内容传递问题

// slot 插槽
// slot 中使用的数据,作用域的问题
// 父模板里调用的数据属性,使用的都是父模板里的数据
// 子模板里调用的数据属性,使用的都是子模板里的数据
// 具名插槽,本身一个插槽只能大的区域去使用,我们可以拆分成几个小片段命名,叫具名插槽
// 写法: <template v-slot:xxx  或 <template #xxx 
// slot存在的价值,比属性props传递简单
//
// 作用域插槽
const app = Vue.createApp({
    data() {
        return {
            text: '提交'
        }
    },
    template: `
    <div>
        <myForm>
            <div>{{text}}</div>  传递给子组件的东西,如果有变量,不会用子组件的变量,而是父组件text的变量  
        </myForm> 
        <myForm>
            <button>{{text}}</button>    
        </myForm>
        <myForm>
        </myForm>
    </div>
    <br>
    <layout>
        <template v-slot:header>
            <div>header</div>
        </template>
        <template #footer>
            <div>footer</div>
        </template>
    </layout>
    <br>
    <list v-slot="slotProps">
        <div>{{slotProps.item}}</div>
    </list>
    <br>
    解构
    <list v-slot="{item}"> 
        <div>{{item}}</div>
    </list>
    `
});


app.component('myForm', {
    methods: {
        handleClick() {
            alert(123);
        }
    },
    template: `
    <div>
        <input />
        <span @click="handleClick">
            <slot>
                default value
                </slot>
        </span>
    </div>
    `
    // slot 标签没有办法绑定事件,需要在外层再写一个标签绑定
    // 要是父组件没有传插槽,就会用 slot 标签里的默认值
});

// 具名插槽
app.component('layout', {
    template: `
    <div>
        <slot name="header"></slot>
        <div>content</div>    
        <slot name="footer"></slot>
    </div>`
});

// 外部组件决定子组件的循环
// 作用域插槽
// 当子组件渲染的内容由父组件决定时,那可以通过作用域插槽实现,让父组件调用子组件的item数据
// 作用域插槽里属性传递的逻辑
app.component('list', {
    data() {
        return {
            list:[1,2,3]
        }
    },
    template: `
    <div>
        <slot v-for="item in list" :item="item"/>
    </div>`
});

const vm = app.mount('#root');

Pasted image 20211029014134.png

总结

插槽:子组件显示的部分DOM与内容由父组件决定

slot使用方式

  1. 🤣 原来我们父组件调用子组件是 < xxx /> 形式,子组件DOM内容是固定的,虽然可以通过父组件传入属性值的方式来修改DOM,但是这样非常麻烦,所以Vue提供了Slot插槽方式
    1. 💡 我们只需将父组件模板 < xxx /> 改成 < xxx> content < /xxx>
      1. ⚠️ content里是插槽DOM内容
      2. ⚠️ 子组件模板要添加 < slot>< /slot> 标签来确定内容放在哪
      3. ⚠️ 插槽标签不能绑定事件,需要在外面加一层标签绑定

    2. 💡 当有多个插槽时,我们可以给插槽具体的名字(具名插槽)
      1. 父组件模板 < xxx> < /xxx>中添加 🔍template 修饰符包裹各个插槽内容,在标签里添加 🔍v-slot:xxxx 表示该插槽的名字
      2. 在子组件模板 < slot>< /slot> 添加 🔍name 属性

    3. 💡 当子组件渲染内容由父组件决定时,那可以通过作用域插槽实现
      1. 作用域插槽属性传递逻辑:父组件规定插槽展示的内容,并规定插槽接收到子组件的值 ⚠️v-slot="slotProps"。子组件使用插槽,可以做循环,并传递循环的值给父组件的插槽内容

⭐ ES6 箭头函数

箭头函数表达式的语法比普通函数表达式更简洁。

(参数1, 参数2, …, 参数N) => { 函数声明 }
(参数1, 参数2, …, 参数N) => 表达式(单一)
// 相当于:(参数1, 参数2, …, 参数N) =>{ return 表达式; }

当只有一个参数时,圆括号是可选的:
(单一参数) => {函数声明}
单一参数 => {函数声明}

没有参数的函数应该写成一对圆括号:
() => {函数声明}

实例

 //ES5
 var x = function(x, y) { return x * y; } 
 //ES6 
 const x = (x, y) => x * y;

有的箭头函数都没有自己的 this。 不适合定义一个 对象的方法

当我们使用箭头函数的时候,箭头函数会默认帮我们绑定外层 this 的值,所以在箭头函数中 this 的值和外层的 this 是一样的。

箭头函数是不能提升的,所以需要在使用之前定义。

使用 const 比使用 var 更安全,因为函数表达式始终是一个常量。

如果函数部分只是一个语句,则可以省略 return 关键字和大括号 {},这样做是一个比较好的习惯:


⭐ JS 异步编程与Promise的使用方式

Pasted image 20211029022610.png

什么是异步编程

我们常常用子线程来完成一些可能消耗时间足够长以至于被用户察觉的事情,比如读取一个大文件或者发出一个网络请求。子线程独立于主线程,所以即使出现阻塞也不会影响主线程的运行。

但是子线程一旦发射了以后就会与主线程失去同步,我们无法确定它的结束,如果结束之后需要处理一些事情,比如处理来自服务器的信息,我们无法将它合并到主线程中

异步操作函数往往通过回调函数来实现异步任务的结果处理

⚠️ 什么是回调函数

回调函数就是一个函数,它是在我们启动一个异步任务的时候就告诉它:等你完成了这个任务之后要干什么。这样一来主线程几乎不用关心异步任务的状态了,他自己会善始善终

⚠️ Promise 函数

JavaScript Promise | 菜鸟教程 (runoob.com)


动态组件与异步组件

⭐ 动态组件

// 动态组件:根据数据的变化,结合 component 这个标签来随时动态切换组件的显示

const app = Vue.createApp({
    data() {
        return {
            currentItem:'input-item'
        }
    },
    methods: {
        handleClick(){
            if(this.currentItem==='input-item')
            this.currentItem = 'common-item';
            else 
            this.currentItem = 'input-item';
        }
    },
    template: `
    动态组件语法(存在之前的输入切换回来后没了的问题,需要引入 keep-alive) <br>
    keep-alive第一次渲染的时候会把输入的状态变更的情况记录下来,缓存
<keep-alive>
    <component :is="currentItem" />
</keep-alive>   
 <br>
    不使用动态组件,不会出现切换回来后没了的问题
    <input-item v-show="currentItem === 'input-item'" />
    <common-item v-show="currentItem === 'common-item'" />
    <button @click = "handleClick">切换</button> 
    `
});


app.component('input-item', {
    template: `
    <div><input /></div>
    `
});

app.component('common-item',{
    template:`
    <div>hello world</div>
    `
});


const vm = app.mount('#root');

Pasted image 20211029025327.png

总结

常规情况下,我们用变量存储显示的组件,然后用v-if、v-show来进行判断,但这种方式比较麻烦

于是我们使用 < component :is="xxx" /> 标签的形式来规定动态组件

但是这种方式来回切换会丢失之前已有输入的数据,需要我们在外层添加一个\< keep-alive>< keep-alive>标签来进行缓存


⭐ 异步组件

const AsyncCommonItem = Vue.defineAsyncComponent(() => {
    return new Promise((resolve, reject)=>{
        setTimeout(()=>{
            resolve({
                template:`<div>this is an async component</div>`
            })
        },4000);
    })
})

const app = Vue.createApp({
    template: `
    <div>
        <common-item />    
        <async-common-item />
    </div>
    `
});

app.component('async-common-item',AsyncCommonItem);

app.component('common-item',{
    template:`
    <div>hello world</div>
    `
});

const vm = app.mount('#root');

Pasted image 20211029025734.png

总结

如何创建异步组件🍔

使用 Vue.defineAsyncComponent( ) 参数是一个返回Promise对象的函数,在Promise中将传递的参数如template传给resolve方法,将这一整个组件定义通过 app.component 方法初始化


💡 基础语法知识点查漏补缺

// v-once 让某个元素标签只渲染一次
// ref 实际上是获取 Dom 节点 / 组件引用的一个语法
// provide / inject 解决多层传递写法冗余麻烦,绕过层层传递,provide:{ },inject:[' ']
const app = Vue.createApp({
    data() {
        return {
            count:222
        }
    },
    /*
    provide:{
        count: 2 // this.count 这样写不行,有问题
    },
    */
    provide(){
        return{
            count: this.count  
            // 这样写才可以,不是双向绑定的,孙子组件拿到的永远是第一次传递得到的值,方法解决后期学习
        }
    }
    ,
    mounted() { // 获取Dom尽量在挂载节点之后获取
        console.log(this.$refs.count);
        console.log(this.$refs.common.sayHello());
    },
    template: `
    <div @click="count++" v-once> 
        {{count}} //v-once
    </div>
    <br>
    <div> 
        <div ref="count">
            {{count}} //ref
        </div>
    </div>
    <br>
    <div> 
        <common-item ref="common" /> //ref 获取子组件引用
    </div>
    <br>
    <div> 
        <child :count="count" />
    </div>
    <button @click="count+=1">ADD</button>
    `
});

app.component('common-item',{
    methods: {
        sayHello(){
            console.log('hello');
        }
    },
    template:`
    <div>hello world</div>
    `
});

app.component('child',{
    template:`
    <child-child />
    `
});

app.component('child-child',{  
    inject:['count'],
    template:`
    <div>{{count}}</div>
    `
});


const vm = app.mount('#root');

Pasted image 20211029032738.png

v-once:

你的标签内容只被渲染一次,后面即使数据发生变化也不重新渲染

ref:


  • 如果页面加载完成了,想要操作dom,为了获取dom节点在外层添加一层标签,添加 ref="name" 后在 mounted 使用 this.$refs.name 进行操作

  • 还可以通过ref获得子组件的引用,调用子组件的方法,维护性不高

provide / inject :


  • 解决多层传递写法冗余麻烦,绕过层层传递,provide:{ },inject:[' ']

  • ⚠️ 要传递data里的值需要将provide改成函数的形式

  • ⚠️ 不是双向绑定,孙子永远拿到第一次传递的值(有解决方法)


🍔基础篇完结

基础篇完结

基础CSS过渡与动画效果

<style>
    /* 动画 */
    @keyframes leftToRight {
        0% {
            transform: translateX(0px);
        }
        50% {
            transform: translateX(-50px);
        }
        100%{
            transform: translateX(0px);
        }
    }
    .animation{
        animation: leftToRight 3s;
    }

    /* 过渡 */
    .transition{
        transition: 3s background-color ease;
    }

    .green{
        background:greenyellow;
    }
    .grey{
        background: grey;
    }
</style>
</head>

<body>
<div id="root"></div>
<script>
// 过渡(背景红变绿,状态到状态)、动画(弹跳移动,运动)
const app = Vue.createApp({
    data() {
        return {
            animate: {
                animation:true,
                transition:true,
                green:true,
                grey:false
            },
            styleObj:{
                background: 'indianred'
            }
        }
    },
    methods: {
        handleClick(){
            //写法一
            this.animate.animation = !this.animate.animation;
            this.animate.green = !this.animate.green;
            this.animate.grey = !this.animate.grey;
            //写法二
            this.styleObj.background =  this.styleObj.background === 'indianred' ? 'yellow':'indianred';
        }
    },
    template: `
    <div> 
        <div :class="animate">hello world</div>
        <div class="transition" :style="styleObj">World</div>
        <button @click="handleClick">切换</button>
    </div>
    `
});

const vm = app.mount('#root');
</script>

Pasted image 20211029050741.png

总结

过渡(背景红变绿,状态到状态)、动画(弹跳移动,运动)

⭐ Vue 实现过渡与动画的两种方式


  • 标签的class进行v-bind,data里添加类对象,触发事件时取反

  • 标签的class固定,对style进行v-bind,data里添加类对象,触发事件时直接对style里的属性进行操作


CSS 过度中ease与ease-in-out的区别(ease曲线)


transition标签

实现单元素组件过渡与动画效果 ①

<style>
    @keyframes shake {
        0%{
            transform: translateX(-100px);
        }
        50%{
            transform: translateX(-50px);
        }
        100%{
            transform: translateX(50px);
        }
    }


    .v-enter-from{
        opacity: 0;
    }
    .v-enter-active{
        transition: opacity 3s ease-out;
        animation: shake 3s;
    }
    /* 过渡的效果 */
    .v-leave-active{
        transition: opacity 3s ease-out;
        animation: shake 3s;           
    }
    .v-enter-to {
        opacity: 1;
    }

    /*
    .v-leave-from{
        opacity: 1;
    }
    */

    .v-leave-to{
        opacity: 0;
    }
</style>
</head>

<body>
<div id="root"></div>
<script>
 // 单元素、单组件的入场、出场动画
const app = Vue.createApp({
    data() {
        return {
            show: false
        }
    },
    methods: {
        handleClick(){
            this.show = !this.show;
    }
},
    template: `
    <div> 
        <transition>   
            <div v-if="show">hello world</div>
        </transition>   
        <button @click="handleClick">切换</button>
    </div>
    `
    // 给transition加name=" ",那么style里面的 v-enter-from 等其他样式的v都要改成name对应的值
});

const vm = app.mount('#root');
</script>

Pasted image 20211029053015.png

总结

使用transition标签

需要添加style样式
v-enter-from, v-enter-active, v-enter-to
v-leave-from, v-leave-active, v-leave-to

⚠️ 过渡需要from, to与active组合,动画需要keyframes与active组合,可以将to的属性写在active里

可以在transition标签里添加name属性,同时修改样式名的 "v"


实现单元素组件过渡与动画效果 ②

// 样式与上文相同
const app = Vue.createApp({
    data() {
        return {
            show: false
        }
    },
    methods: {
        handleClick(){
            this.show = !this.show;
    }
},
    template: `
    <div> 
        <transition :duration="{enter:1000, leave:3000}">   
            <div v-show="show">hello world</div>
        </transition>   
        <button @click="handleClick">切换</button>
    </div>
    `
});

const vm = app.mount('#root');

总结

🍞 transition 标签里还可以指定样式
Pasted image 20211029060023.png
从而可以做复杂的CSS动画,使用复杂的CSS库
Animate.css | A cross-browser library of CSS animations.

⭐ 过渡与动画一方做完直接结束另外一方的方法 Pasted image 20211029061857.png
给 transition 添加 type 属性,可以是 transition , animation

Pasted image 20211029062029.png
给 transition 添加 :duration 属性,不管 transition 与 animation 进行几秒

Pasted image 20211029062106.png
:duration 属性可以写成对象,对enter与leave分开规定


实现单元素组件过渡与动画效果 ③

⭐ 我不想用css的动画效果,想用js的动画效果

Pasted image 20211029062440.png

怎么用js的动画效果:钩子(类似生命周期函数),在某些时刻自动调用

const app = Vue.createApp({
    data() {
        return {
            show: false
        }
    },
    methods: {
    handleClick(){
              this.show = !this.show;
    },
    handleBeforeEnter(el){  // 可以接收el
        el.style.color="red";
    },
    handleEnterActive(el,done){    // 可以接收el 与 done
        const animation = setInterval(() => {
            const color = el.style.color;
            el.style.color = color ==='red' ? 'green':'red';
        }, 1000);
        setTimeout(() => {
            clearInterval(animation);
            done();
        }, 3000);
    },
    handleAfterEnter(){  // 可以接收 el
        console.log(123); //必须要结束动画的时候调用done才能感知到
    }
},
    template: `
    <div> 
        <transition :css="false" 
        @before-enter="handleBeforeEnter"
        @enter="handleEnterActive"
        @after-enter="handleAfterEnter"
        @before-leave=""
        @leave=""
        @after-leave=""

        >   
            <div v-show="show">hello world</div>
        </transition>   
        <button @click="handleClick">切换</button>
    </div>
    `
});

const vm = app.mount('#root');

总结

使用 :css="false" 关闭css样式,然后使用钩子(类似生命周期函数)来规定动画前,动画时,动画结束后执行的函数

Pasted image 20211029064325.png

Pasted image 20211029064409.png
⚠️ 调用AfterEnter或AfterLeave需要在Active函数里调用 done()


😜 Vue 学习笔记 5

VSC Keyboards

ctrl shift .  
ctrl + p + @  
 quokka  
用于查找大文件里面的内容与调试  
Ctrl G 换行号  
Ctrl D 选中单词  
ctrl L   =  Highlight line  
当终端出现错误时,按 ctrl + K 清除,然后按向上键  
ALT + 左键 | 鼠标侧键 返回上一次编辑处

Pasted image 20211117221224.png Pasted image 20211117221238.png Pasted image 20211117221241.png

// 自定义代码片段
// https://snippet-generator.app/
{
    "My Vue Html": {
        "prefix": "myvue",
        "body": [
          "<!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>$1</title>",
          "    <script src=\"https://unpkg.com/vue@next\"></script>",
          "    <script src=\"https://unpkg.com/axios/dist/axios.min.js\"></script>",
          "</head>",
          "<body>",
          "$2",
          "</body>",
          "<script src=\"./$3\"></script>",
          "</html>",
          ""
        ],
        "description": "My Vue Html"
      }
}

动画实现

组件和元素切换动画的实现

.v-enter-from,.v-leave-to{
    opacity: 0;
}
.v-enter-active,.v-leave-active{
    transition: opacity 1s ease-in;
}
.v-enter-to,.v-leave-from{
    opacity: 1;
}
// 多个单元素标签之间的切换
// 多个单组件之间的切换

const ComponentA = {
    template : '<div>hello world</div>'
}
const ComponentB = {
    template : '<div>bye world</div>'
}

const app = Vue.createApp({
    data() {
        return {
            show: false,
            component : 'component-a'
        }
    },
    methods: {
    handleClick(){
              this.show = !this.show;
              this.component = this.component == 'component-a' ? 'component-b' : 'component-a';
        }
    },
    components:{
        "component-a":ComponentA,
        "component-b":ComponentB
    },
    template: `
    <div> 
        <transition mode="out-in" appear>   
            <component :is="component" />
        </transition>   
        <button @click="handleClick">切换</button>
    </div>
    `
    // <div v-if="show">hello world</div>
    // <div v-else="show">bye world</div>
            // <component-a v-if="show" />
            // <component-b v-else="show" />
});

const vm = app.mount('#root');

UcWWIfHdWq.gif

总结

隐藏的单元素单组件和要展现的单元素单组件过渡是同步执行的
所以给transition添加 mode 属性 in-out 和 out-in
希望打开页面时第一次出现元素也显示动画,给transition添加 appear
不仅可以用 v-if 实现单元素,单组件切换,也可以直接用 component :is 实现动态组件切换,过渡动画同样生效

列表动画 transition-group

.list-item {
    display: inline-block;
    /*在一行内显示需要用 inline-block 不然动画效果实现不了*/
    margin-right: 10px;
}

.v-enter-from {
    opacity: 0;
    transform: translateY(30px);
}

.v-enter-active {
    transition: all .5s ease-in;
}

.v-enter-to {
    opacity: 1;
    transform: translateY(0);
}

.v-move{
    transition: all .5s ease-in;
}
// 列表动画的实现

const app = Vue.createApp({
    data() {
        return {
            list: [1, 2, 3]
        }
    },
    methods: {
        handleClick() {
            this.list.unshift(this.list.length + 1);
        }
    },
    template: `
    <div> 
        <transition-group appear>
             <span class="list-item" v-for="item in list" :key="item">{{item}}</span>
        </transition-group>
        <button @click="handleClick">增加</button>
    </div>
    `
});

const vm = app.mount('#root');

fifIpp3YWN.gif

总结

使用span标签时需要将 display 改成 inline-block 否则无法显示动画

给v-for外面包一层 transition-group 标签应用列表动画
新增了 .v-move {} 用来为其他元素加动画

状态动画

// 状态动画的实现,svg

const app = Vue.createApp({
    data() {
        return {
           number:1,
           animateNumber : 1
        }
    },
    methods: {
        handleClick() {
            this.number = 10;
            if(this.animateNumber<this.number){
            const animation = setInterval(()=>{
                this.animateNumber +=1;
                if(this.animateNumber==10){
                    clearInterval(animation);
                }
            },100);
          }
        }
    },
    template: `
    <div> 
        <div>{{animateNumber}}</div>
        <button @click="handleClick">增加</button>
    </div>
    `

});

const vm = app.mount('#root');

NZjiB29sk0.gif

Mixin 混入语法

基础语法与优先级(局部)

// mixin 混入
// 组件 data,methods 优先级高于 mixin data,methods 优先级 
// :当你混入了一些内容,如果与组件里的内容有冲突,优先使用组件的
// 生命周期函数,先执行 mixin 里面的,再执行组件里面的
const myMixin = {
    data(){
        return {
            number : 2,
            count : 10
        }
    },
    created() {
        console.log('mixin created');
    },
    methods: {
        handleClick(){
            console.log('mixin handle Click');
        }
    },
}

const app = Vue.createApp({
    data() {
        return {
           //number:1
        }
    },
    created(){
        console.log('created');
    },
    mixins:[myMixin],
    methods: {
        // handleClick() {
        //     console.log('handleClick');
        // }
    },
    template: `
    <div> 
        <div>{{number}}</div>
        <div>{{count}}</div>
        <button @click="handleClick">增加</button>
    </div>
    `

});

const vm = app.mount('#root');

总结

⭐ mixin 混入

组件 data,methods 优先级高于 mixin data,methods 优先级
( 当你混入了一些内容,如果与组件里的内容有冲突,优先使用组件的 )
生命周期函数,先执行 mixin 里面的,再执行组件里面的

局部混入

组件里设置属性 mixins 局部混入
`js
const myMixin = {
data(){
return {
count : 10,
number:1
}
}
}

const app = Vue.createApp({ mixins:[myMixin], template: <div> <div>{{number}}</div> <child /> </div>

});

app.component('child',{ //mixins:[myMixin], template:<div>{{count}}</div> });

const vm = app.mount('#root');

### 全局混入
>组件外使用 app.mixin 全局混入
```js
    <div id="root"></div>
    <script>
        const app = Vue.createApp({
            //mixins:[myMixin],
            template: `
            <div> 
                <div>{{number}}</div>
                <child />
            </div>
            `

        });

        app.component('child',{
            //mixins:[myMixin], 
            template:`<div>{{count}}</div>`
        });

        app.mixin({            
            data(){
                return {
                    count : 10,
                    number:1
                }
            }
        });

        const vm = app.mount('#root');

自定义属性与读取

// 自定义属性,组件中的属性优先级高于 mixin 属性的优先级
const myMixin = {
    number : 1
}

const app = Vue.createApp({
    number: 2,
    template: `
    <div> 
        <div>{{this.$options.number}}</div>
    </div>
    `
});

const vm = app.mount('#root');

直接定义在组件最外层叫自定义属性

this.$options 指的是 vue组件属性所有内容

修改混入优先级

// 自定义属性,组件中的属性优先级高于 mixin 属性的优先级
const myMixin = {
    number : 1
}

const app = Vue.createApp({
    number: 2,
    mixins:[myMixin],
    template: `
    <div> 
        <div>{{this.$options.number}}</div>
    </div>
    `
});

// 修改优先级
app.config.optionMergeStrategies.number = (mixinVal,appVal) =>{
    return mixinVal || appVal;
};

const vm = app.mount('#root');

app.config.optionMergeStrategies.number = (mixinVal,appVal) =>{
return mixinVal || appVal;
};

💡 少用Mixin


  • 逻辑上看起来不是直观清晰,代码浏览麻烦

  • 出问题了不仅要看组件,还要看mixin有没有冲突

  • 全局mixin对后期开发的影响( 很长时间后忘了定义mixin )


⭐ 自定义指令

从聚焦谈自定义指令

ref

const app = Vue.createApp({
    mounted() {
        this.$refs.input.focus();
    },
    template: `
    <div> 
        <input ref="input"/>
    </div>
    `
});

const vm = app.mount('#root');

这种聚焦逻辑无法被复用 假如还有其他input需要再写一遍 $refs.

directive

const app = Vue.createApp({
    directives: directives,
    template: `
    <div> 
        <input v-focus/>
    </div>
    `
});

app.directive('focus',{
    mounted(el){ 
        el.focus();
    }
});

const vm = app.mount('#root');

全局指令

app.directive('focus',{
    mounted(el){ 
        el.focus();
    }
});

局部指令

const directives = {
    focus:{
        mounted(el){ 
        el.focus();
      }
    }
}

const app = Vue.createApp({
    directives: directives,
    template: `
    <div> 
        <input v-focus/>
    </div>
    `
});

注意,组件里 directives 接收的是一个对象,对象里可以写多个指令

指令生命周期函数

const app = Vue.createApp({
    //directives: directives,
    data() {
        return {
            hello : true
        }
    },
    template: `
    <div> 
        <div v-if="hello">
            <input v-focus/>
        </div>
    </div>
    `
});

// 定义 focus 全局指令
app.directive('focus',{
    beforeMount() {
        console.log('beforeMount');
    },
    mounted(el){ 
        console.log('mounted');
        el.focus();
    },
    // 元素或组件上数据发生变化,重新渲染
    beforeUpdate() {
        console.log('beforeUpdate');
    },            
    updated() {
        console.log('updated');
    },
    // 元素或组件被销毁
    beforeUnmount() {
        console.log('beforeUnmount');
    },
    unmounted() {
        console.log('unmounted');
    },
});

const vm = app.mount('#root');

beforeMount
mounted 数据变化时,mounted不会重新执行
beforeUpdate 元素或组件上数据发生变化,重新渲染
updated
beforeUnmount 元素或组件被销毁
unmounted

💡 binding 使用

const app = Vue.createApp({
    data() {
        return {
            distance : 110
        }
    },
    template: `
    <div> 
        <div v-pos2:left="distance" class="header">
            <input />
        </div>
    </div>
    `
});

app.directive('pos',{
    // 数据变化时,mounted不会重新执行
    mounted(el,binding) {
        // el.style.distance = "200px"; // 不是特别灵活
        el.style.top = binding.value + 'px';
    },
    updated(el,binding) {
        el.style.top = binding.value + 'px';
    },
});

//简写,当有 mounted 与 updated 且代码相同
app.directive('pos2',(el,binding)=>{
    console.log(binding.arg); // 输出 : 后面内容
    el.style[binding.arg] = binding.value + 'px';            
})
const vm = app.mount('#root');

简写,当有 mounted 与 updated 且代码相同

app.directive('pos2',(el,binding)=>{

})


v-focus:abc="123"
binding.value 获取 = 后面的值 123,binding.arg 获取 : 后面的值 abc

知识点


  • 如何去定义一个指令

  • 生命周期函数

  • 参数值的获取

  • mounted+updated的简写


Teleport 传送门(蒙层)

<style>
        .area{
            position:absolute;
            top:50%;
            left: 50%;
            transform: translate(-50%,-50%);
            /* 写两个transform 下面的会覆盖前面的,所以用 translate */
            width: 200px;
            height: 300px;
            background: green;
        }
        .mask{
            position: absolute;
            left: 0;
            right: 0;
            top: 0;
            bottom: 0;
            background: #000;
            opacity: 0.5;
            color: aliceblue;
        }
</style>
<body>
    <div id="root"></div>
    <div id="hello"></div>
    <script>
        const app = Vue.createApp({
            data() {
                return {
                    show:false
                }
            },
            methods: {
                handleBtnClick(){
                    this.show = !this.show;
                }
            },
            template: `
            <div class="area"> 
                <button @click="handleBtnClick">按钮</button>
                <teleport to="#hello">
                    <div class="mask" v-show="show">test word</div>
                </teleport>
            </div>
            `
        });

        const vm = app.mount('#root');
    </script>
</body>

如果不加 teleport 创建出来的蒙层只会把class='area'的div覆盖

需要把mask teleport到body层

于是使用 teleport to='body' 也可以是 #xxx

底层 render 函数(选学)

传入参数生成<h+id>标签

原本方法

const app = Vue.createApp({
    template: `
    <my-title :level="1">
        hello
    <my-title/>
    `
});

app.component('my-title',{
    props:['level'],
    template:`
    <h1 v-if="level===1"><slot /></h1>
    <h2 v-if="level===2"><slot /></h2>
    <h3 v-if="level===3"><slot /></h3>
    <h4 v-if="level===4"><slot /></h4>
    <h5 v-if="level===5"><slot /></h5>
    `
})

const vm = app.mount('#root');

:level='1' 加了冒号是 表达式 传递过去的是数字
子组件 this.$slots 获取传递过来的插槽

使用h函数简单化

const app = Vue.createApp({
    template: `
    <my-title :level="1">
        hello dell
    </my-title>
    `
});

app.component('my-title',{
    props:['level'],
    render() {
        const {h} = Vue;
        // 虚拟DOM JS对象
        // {
        //     tagName : 'h1',
        //     text : hello,
        //     attributes : {}
        // }
     return h('h'+this.level,{},[this.$slots.default(),h('h4',{},'www')])
    }
    // template:`
    // <h1 v-if="level===1" name="123"><slot /></h1>
    // <h2 v-if="level===2"><slot /></h2>
    // <h3 v-if="level===3"><slot /></h3>
    // <h4 v-if="level===4"><slot /></h4>
    // <h5 v-if="level===5"><slot /></h5>
    // `
})

const vm = app.mount('#root');

虚拟DOM : 性能更快、跨平台


虚拟dom是对真实dom的映射 一个 js对象

Render 过程

template -> render -> h -> 虚拟DOM -> 真实DOM -> 展示到页面上

① template 被解析成 render 函数
② render 函数调用h函数生成虚拟dom js对象
③ 虚拟dom生成真实dom
④ 真实dom展示到页面上


⭐ 插件的定义与使用

基础语法

//plugin 插件,也是把通用性功能封装起来
const myPlugin = {
    install(app,options){
        app.provide('name','Dell Lee');
        app.directive('focus',{
            mounted(el) {
                el.focus();
            },
        });
        app.mixin({
            mounted(){
                console.log('123'); //每个组件都会触发mixin的生命周期函数
            }
        });
        app.config.globalProperties.$sayHello = 'hello world'; 
        console.log(app,options);
    }
}

const app = Vue.createApp({
    template: `
    <my-title />
    `
});

app.component('my-title',{
    inject:['name'],
    mounted() {
        console.log(this.$sayHello); // 底层扩展
    },
    template: `<div>{{name}} world<input v-focus /></div>`
});

app.use(myPlugin,{name:'dell'});

const vm = app.mount('#root');

插件 就是把通用性的功能封装起来

写插件就是写一个对象 必须要有一个install方法 参数是 app 和 options

app是使用插件对应的vue实例

options 是传递进去的对象

app.use(xxx, 传递自定义参数对象)

可以直接对app进行扩展
app.provide('name','dell lee')
( 组件里不能直接使用 需要用 inject:['name'] 声明一下 )
扩展指令
app.directive

每个组件都会触发mixin的生命周期函数

app.config.globalProperties.$ sayHello

扩展全局属性 因为是对底层做扩展加 $ 作为私有的东西
子组件里使用 this.$ sayHello

数据校验插件开发实例

// 对数据做校验的插件

const app = Vue.createApp({
    data() {
        return {
            name: 'dell',
            age: 23
        }
    },
    rules: {
        age: {
            validate: age => age < 25,
            message: '太年轻了'
        },
        name: {
            validate: name => name.length <= 4,
            message: '字符串太短了'
        }
    },
    template: `<div>{{name}},{{age}}</div>`
});

// 简写
const ValidatorPlugin = (app, options) => {
    app.mixin({
        created() {
            for (let key in this.$options.rules) {
                const item = this.$options.rules[key];
                this.$watch(key, (value) => {
                    if (item.validate(value)) console.log(item.message);
                })
                //console.log(key,item);
            }
        }
    })
}

app.use(ValidatorPlugin);

const vm = app.mount('#root');

install(app,options){ } 可以简写成 (app, options) => { }

this.$watch(key, (value) => {
if (item.validate(value)) console.log(item.message);
})

只写一个mixin看不出在干什么
用插件封装可读性和可扩展性比较好

2021.11.17

😜 Composition API

VSC

Ctrl Space 触发建议 Esc 返回
Ctrl Shift Space 触发提示

Setup 函数的使用

为什么使用 Composition API

当组件变成几百行时候 找逻辑比较困难
传统vue语法维护性低

const app = Vue.createApp({
    template: `<div @click="handleClick">{{name}}</div>`,
    methods: {
        test(){
          console.log(this.$options);
          console.log(this.$options.setup());
        }
    },
    mounted() {
        this.test();
    },
    // created 实例被完全创建之前
    setup(props,context){
        // console.log(this); // 是 window
        // this.test(); 不能执行
        return {
            name : 'dell',
            handleClick: ()=>{
                alert(123);
            }
        }
    }
});

const vm = app.mount('#root');

composition api 使用前提必须用 setup 函数
/

setup在created 实例被完全创建之前执行
setup内部不能用 this
应用实例 $options 上挂载着 setup 对象

⭐ 响应式引用(ref,reactive)

ref,reactive

// ref,reactive 响应式的引用
// 原理,通过 proxy 对数据进行封装,当数据变化时,触发模板等内容的更新
// ref 处理基础类型的数据,字符串,数字
// reactive 处理非基础类型的数据,对象,数组
const app = Vue.createApp({
    template: `<div>{{name}} {{nameObj.name}}</div>`,
    // 这里不写 name.value 因为Vue会自己做处理,如果是个响应式引用Vue会自动调用 .value
    setup(props,context){
        // 写法一
        // const {ref} = Vue;
        // // proxy , 'dell' 变成 proxy({value:'dell'}) 这样的一个响应式引用
        // let name = ref('dell');
        // setTimeout(()=>{
        //     name.value = 'lee'; // name 变成了个对象,修改的时候加 .value
        // },2000);
        // return {
        //     name //: 'dell'
        // }

        // 写法二
        const {reactive} = Vue;
        // proxy , {name : 'dell'} 变成 proxy({name : 'dell'}) 这样的一个响应式引用
        const nameObj = reactive({name : 'dell'});
        setTimeout(()=>{
            nameObj.name = 'lee';
        },2000);
        return {
            nameObj
        };
    }
});

const vm = app.mount('#root');

如何把普通变量改成响应式变量
ref,reactive 响应式的引用

通过 proxy 对数据进行封装,当数据变化时,触发模板等内容的更新
ref 处理基础类型的数据,字符串,数字
reactive 处理非基础类型的数据,对象,数组

proxy , 'dell' 变成 proxy({value:'dell'}) 这样的一个响应式引用
let name = ref('dell');

name.value = 'lee'; name 变成了个对象,修改的时候加 .value

如果是个响应式引用Vue会自动调用 .value

readonly

const {reactive,readonly} = Vue;
const nameObj = reactive({name : 'dell'});
const copyNameObj = readonly(nameObj);
// proxy , {name : 'dell'} 变成 proxy({name : 'dell'}) 这样的一个响应式引用

setTimeout(()=>{
    nameObj.name = 'lee';
    copyNameObj.name = 'lee';
},2000);

Pasted image 20211119043047.png

toRefs

const {reactive,readonly,toRefs} = Vue;
const nameObj = reactive({name : 'dell',age:28});

setTimeout(()=>{
    nameObj.name = 'lee';
},2000);

// toRefs proxy({name : 'dell',age:28}) => 
// { 
//   name : proxy({value:'dell'}),
//   age : proxy({value:28})
// }
const {name} = toRefs(nameObj);
return {
    name
}

创建的reactive对象直接解构不具备响应式,需要调用 toRefs 进行转化

toRefs proxy({name : 'dell',age:28}) =>
{
name : proxy({value:'dell'}),
age : proxy({value:28})
}

toRef

const app = Vue.createApp({
    template: `<div>{{age}} </div>`,
    setup(props,context){
        const {reactive,toRef} = Vue;
        const data = reactive({name : 'dell'});
        const age = toRef(data,'age');
        setTimeout(()=>{
            age.value = 'lee' 
            // 可能出现的对象里面没有属性值,就可以用toRef代替toRefs,但不建议这样写,直接给对象添加age:''
        },2000);
        return {
            age
        }
    }
});

const vm = app.mount('#root');

可能出现的对象里面没有属性值,就可以用toRef代替toRefs,但不建议这样写,直接给对象添加 age:' '
const age = toRef(data,'age');

setup context 参数

const app = Vue.createApp({
    methods: {
        handleChange(){
            alert('change');
        }
    },
    template: `<child @change="handleChange" app='app'>
                   parent
                </child>
                `, // 不用props接收变成 none-props 属性
});

app.component('child', {
    template: `
    <div @click="handleClick">child</div>
    `,
    setup(props, context) {
        const {h} = Vue;
        const { 
            attrs,  // this.$attrs
            slots,  // this.$slots
            emit    // this.$emit
        } = context;
        function handleClick(){
            emit('change')
        }
        return {
            handleClick
        }
        // console.log(attrs, slots.default(), emit);
        // return ()=>h('div',{},slots.default());
    }
})
const vm = app.mount('#root');

实现 ToDoList(优雅的写法)

// 关于 list 操作的内容进行了封装
const listRelativeEffect = () => {
    const {
        reactive
    } = Vue;
    const list = reactive([1, 2, 3]);
    const addItemToList = (item) => {
        list.push(item); //list.push(inputValue.value); 耦合了,不建议
    }
    return {
        list,
        addItemToList
    }
}

// 关于 inputValue 操作的内容进行了封装
const inputRelativeEffect = () =>{
    const {ref} = Vue;
    const inputValue = ref('123');
    const handleInputValueChange = (e) => {
        inputValue.value = e.target.value;
    }          
    return {inputValue,handleInputValueChange}
}

const app = Vue.createApp({
    setup() {
        // 这个函数读得很变扭,我们可以把里面的处理进行拆分
        // const {ref,reactive} = Vue;
        // const inputValue = ref('123');
        // const list = reactive([1,2,3]);
        // const handleInputValueChange = (e) => {
        //     console.log('change',e.target.value);
        //     inputValue.value = e.target.value;
        // }
        // const handleSubmit = () =>{
        //     list.push(inputValue.value);
        // }

        // 流程调度中转
        const {list,addItemToList} = listRelativeEffect();
        const {inputValue,handleInputValueChange} = inputRelativeEffect();

        return {
            list,addItemToList,
            inputValue,handleInputValueChange
        }
    },
    template: `
    <div>
        <div>
            <input :value="inputValue" @input="handleInputValueChange" />
            <div>{{inputValue}}</div>
            <button @click="addItemToList(inputValue)">提交</button>
        </div>
        <ul>
            <li v-for="(item,index) in list" :key="index">{{item}}</li>
        </ul>
    </div>
    `
});

const vm = app.mount('#root');
</script>

总结

5ZF1YPkssC.gif

原先 setup() 这个函数读得很变扭,我们可以把里面的处理进行拆分

使其专门用于 流程调度中转 的作用

可维护性的提升 把数据操作提取出来到各自的函数里

Computed 计算属性

const app = Vue.createApp({
    setup() {
        const {ref,computed,reactive} = Vue;
        const count = ref(0);
        const countObj = reactive({count:0});
        const handleClick = ()=>{
            count.value +=1;
            countObj.count+=1;
        }
        const countAddFive = computed(()=>{
            return count.value + 5;
        });
        let countAddFive2 = computed({
            get : ()=>{
                return count.value + 5;
            },
            set : (param)=>{
                count.value = param-5;
            }
        })
        let countAddFive3 = computed({
            get : ()=>{
                return countObj.count + 5;
            },
            set : (param)=>{
                countObj.count = param - 5;
            }
        })
        setTimeout(()=>{
            countAddFive2.value = 100;
            countAddFive3.value = 100;
        },3000);


        return {
            countAddFive3,
            countAddFive2,
            countAddFive,
            handleClick,
            count,
            countObj
        }
    },
    template: `
    <div>
        <span @click="handleClick">{{count}}---{{countObj.count}}</span> --- {{countAddFive2}} --- {{countAddFive3}}
    </div>
    `
});

const vm = app.mount('#root');

Watch

const app = Vue.createApp({
    setup() {
        const {ref,watch,reactive,toRefs} = Vue;

        const nameObj = reactive({name:'dell',englishName:'lee'}); //ref('dell');
        // 具备一定的惰性 lazy 
        // 参数可以拿到原始和当前值
        watch([()=>nameObj.name,()=>nameObj.englishName],(newValue,oldValue)=>{ //([curName,curEng],[prevName,PreEng])
            console.log(newValue,oldValue);
        })
        const {name,englishName} = toRefs(nameObj);

        return {
            name,englishName
        }
    },
    template: `
    <div>
        <div>
        Name: <input v-model="name" />
        </div>
        <div>
        Name is {{name}}
        </div>
        <div>
        EnglishName: <input v-model="englishName" />
        </div>
        <div>
        EnglishName is {{englishName}}
        </div>
    </div>
    `
});

const vm = app.mount('#root');

🍔 后续补充

😜 Vue-cli

Vue-Cli 安装

npm install -g vue@cli

生成项目

vue create demo1

...

npm run serve

安装 Vetur 工具

Pasted image 20211120014850.png

单文件组件

<template>
  <img alt="Vue logo" src="./assets/logo.png">
  <HelloWorld msg="Welcome to Your Vue.js App"/>
</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>

编写 ToDoList

<template>
  <div>
  <input v-model="inputValue" />
  <button class="button" @click="handleAddItem">提交</button>
  </div>
  <ul>
    <list-item v-for="(item,index) in list" :key="index" :msg="item.toString()"></list-item>
  </ul>
</template>

<script>
import { reactive, ref } from '@vue/reactivity';

import ListItem from './components/ListItem.vue';

// @ 单文件组件

export default {
  name: 'App',
  components:{
    ListItem
  },
  setup() {
    const inputValue = ref('');
    const list = reactive([1,2,3]);

    const handleAddItem = ()=>{
      list.push(inputValue.value);
      inputValue.value=''
    };

    return {list,inputValue,handleAddItem}
  }
}
</script>

<style>
  .button{
    margin-left: 20px;
    color:indianred;
  }
</style>

<template>
    <li class="button">{{ msg }}</li>
</template>

<script>
export default {
  name: 'ListItem',
  props: {
    msg: String
  }
}
</script>

<style>

</style>

⭐ Router

APP.VUE

<template>
  <div id="nav">
    <!-- // @ router-link 只是跳转路由的标签 -->
    <router-link to="/">Home</router-link> |
    <router-link to="/about">About</router-link> |
    <router-link to="/login">Login</router-link>
  </div>
  <!-- // @ router-view 负责展示当前路由对应的组件内容 -->
  <router-view/>
</template>

<style>
</style>

MAIN.JS

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

// @ 路由是根据URL的不同,展示不同的内容
createApp(App).use(router).mount('#app')

INDEX.JS

import { createRouter, createWebHashHistory } from 'vue-router'
import Home from '../views/Home.vue'
import Login from '../views/Login.vue'

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    // @ 异步加载路由
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  },
  {
    path: '/login',
    name: 'Login',
    component: Login
  }
]

const router = createRouter({
  history: createWebHashHistory(),
  routes
})

export default router

⭐ Vue-X

使用全局仓库数据

store/index.js

import { createStore } from 'vuex'
// @ VueX 数据管理框架
// @ VueX 创建了一个全局唯一的仓库,用来存放全局的数据
export default createStore({
  state: { //@ 全局数据 
    name : 'dell'
  },
  mutations: {
  },
  actions: {
  },
  modules: {
  }
})

Home.vue

<template>
  <div class="home">
    <img alt="Vue logo" src="../assets/logo.png">
    <h1>{{myName}}</h1>
  </div>
</template>

<script>

export default {
  name: 'Home',
  // @ 怎么用 全局仓库里面的数据
  computed:{
    myName(){
      return this.$store.state.name
    }
  }
}
</script>

About.vue

<template>
  <div class="about">
    <h1>This is an about page {{myName}}</h1>
  </div>
</template>

<script>
export default {
  name: 'About',
  // @ 怎么用 全局仓库里面的数据
  computed:{
    myName(){
      return this.$store.state.name
    }
  }
}
</script>

修改全局仓库数据

全局仓库 = 图书馆 ( 按照机制修改数据)

store/index.js

import { createStore } from 'vuex'
// @ VueX 数据管理框架
// @ VueX 创建了一个全局唯一的仓库,用来存放全局的数据
export default createStore({
  state: { //@ 全局数据 
    name : 'dell'
  },
  // @ commit 和 mutation 做关联
  mutations: {
    // @ 第四步,对应的 mutation 被执行
    change(){  // @ (state,str)
      // @ 第五步,在 mutation 里面修改数据
      console.log('mutation');
      // warn mutation 里面只允许写同步代码,不写异步代码
     // setTimeout(()=>{
        this.state.name = 'lee' // ! state.name
     // },2000)

    }
  },
  // @ dispatch 和 actions 做关联
  actions: {
    // @ 第二步,store感知到你触发了一个叫做change的action,执行change方法
    change(){ // @ (store,str)
      // @ 第三步,提交一个commit,触发一个 mutation
      setTimeout(()=>{
      this.commit('change'); // ! store.commit
      },2000)
    }
  },
})

About.vue

<template>
  <div class="about">
    <h1 @click="handleClick">This is an about page {{myName}}</h1>
  </div>
</template>

<script>
export default {
  name: 'About',
  // @ 怎么用 全局仓库里面的数据
  computed:{
    myName(){
      return this.$store.state.name
    }
  },
  methods:{
    handleClick(){
      // @ 第一步,想改变数据, vuex 要求
      // step 使用 dispatch 方法,必须派发一个 action 名叫 change
      // step 感知到 change 这个 action ,执行 store 中 actions 下面的 change 方法 
      // step 提交 commit 一个叫做 change 的数据改变
      // step mutation 感知到提交的 change 改变,执行 change 方法改变数据
      this.$store.dispatch('change');

      // @ 同步修改可以直接
      // @ this.$store.commit('change');
    }
  }
}
</script>

总结

什么是 vuex?

把整个应用里面共用的数据保存

数据放到state里面去

改数据的五个步骤
dispatch,action(异步),commit(同步),mutation,修改数据

Composition API

store/index.js

import { createStore } from 'vuex'
// @ VueX 数据管理框架
// @ VueX 创建了一个全局唯一的仓库,用来存放全局的数据
export default createStore({
  state: { 
    name : 'dell'
  },
  mutations: {
    change(){  // @ (state,str)
      this.state.name = 'lee' 
    },
    changeName(state,str){  // @ (state,str)
      state.name = '老王' ;
      console.log(str);
    }
  },
  actions: {
    getData(store){
        store.commit('changeName','hello');
    }
  }
})

Home.vue

<template>
  <div class="home">
    <img alt="Vue logo" src="../assets/logo.png">
    <h1 @click="handleClick">{{name}}</h1>
  </div>
</template>

<script>
import { toRefs } from '@vue/reactivity';
import { useStore } from "vuex"

export default {
  name: 'Home',
  // @ 怎么用 composition api 全局仓库里面的数据
  setup() {
    const store = useStore();
    const {name} = toRefs(store.state);
    const handleClick= ()=>{
      //store.commit('changeName');
      store.dispatch('getData');
    }
    return {name,handleClick}
  }
}
</script>

Axios 发送AJAX请求

import axios from "vuex"
axios.get('http://getman.cn/echo')
.then(res => {
  console.log(res)
}).catch(err=>{
  console.log(err);
});

11.20

😜 Node.js 学习笔记 1

VSC KeyBoards

Alt + ← 或 鼠标侧键 返回上一个光标

NodeJS 的安装配置

https://www.runoob.com/nodejs/nodejs-install-setup.html


NodeJS 如何运行JS脚本

💡 使用命令提示符


  • win+r打开运行,输入cmd按回车

  • 输入盘符,切换到目标文件夹,如 F:

  • 输入 cd 路径 ,如 cd NodeJS/1-1 (cd是 change directory 的缩写)

  • 输入 ls 先检查当前目录下有无对应文件 (cat index.js 输出该文件内容)

  • 输入 node index.js 即js文件名字进行运行


💡 使用VSC自带的调试器,按F5后选择NodeJS开始调试
Pasted image 20211030012429.png


HTTP协议在做什么

客户端和后端服务器是通过一个叫HTTP的通信协议进行数据传输 \ HTTP 中文名叫 超文本传输协议

在传输数据前,客户端与服务端必须先建立 TCP 连接 \ 客户端在发送请求的时候,必须依照HTTP协议里的请求格式(否则服务器将无法识别)

⚠️ HTTP 协议就是在帮助我们标准化客户端和后端服务器的数据交换流程

怎么查看页面的请求与响应,在Chrome开发者工具里切换到NetWork panel

Pasted image 20211030013137.png

GET请求主要是从后端服务器里获取数据(源代码、视频、JSON数据等)

Status Code:状态码,🟢 200 代表请求成功


编写第一个 NodeJS 文件

⚠️ NodeJS 后端程序文件修改后刷新浏览器不生效,需要重启后端程序才生效

const http = require("http"); //导入http模块

const server = http.createServer((request,response) =>{  // 使用模块创建NodeJS后端服务器
    response.end("Hello From NodeJS Server 2");
});

const port = 3000;
const ip = "127.0.0.1";

server.listen(port,ip,()=>{  
 console.log(`Server is running at http://${ip}:${port}`); //es6使用使${}来包裹一个变量或者一个表达式,字符串需要使用反引号 `
});  //服务器在运行时监听端口3000,从这个端口收集来自客户端的请求

Pasted image 20211030014155.png


🌎 对后端程序进行修改

🍔 添加三个HTML文件,服务器会根据用户想访问的页面读取对应的HTML文件,然后把这些HTML源代码返回给客户端

Pasted image 20211030014653.png

request.url、method

我们先给后端服务器添加一行 console.log(request.url); 代码

Pasted image 20211030014824.png

Pasted image 20211030014929.png
每当用户访问页面,NodeJS后端程序知道用户需要访问哪个页面


💡 request 还有一个属性叫 method 可以用来判断用户请求是 GET 还是 POST
Pasted image 20211030015056.png
Pasted image 20211030015116.png

搭配使用 request.url 与 request.method , 就可以在NodeJS里得到用户请求的页面与请求方法

编写 sendResponse 响应

🍔 编写 sendResponse 函数(ES6 箭头函数)
Pasted image 20211030015654.png
filename 文件名, statusCode 状态码, response 是 响应对象


⚠️ 为了读取服务器里的文件,我们需要导入fs模块(📁File System)
Pasted image 20211030015854.png

然后我们就可以使用 fs.readFile(文件路径,回调函数) 读取文件,回调函数还有两个参数分别是error、data(如果成功读取文件data是undefined,data会被赋予文件内容;如果读取的时候报错,error就会赋予报错信息)

所以要先判断有没有错误,如果发生错误返回响应信息
Pasted image 20211030021528.png

const http = require("http"); //导入http模块
const fs = require('fs');  //导入fs模块

const sendResponse = (filename,statusCode,response) => {
    fs.readFile(`F:/NodeJS/Study/common/1-1/html/${filename}`,(error,data)=>{
        console.log(error);
        if(error){
            response.statusCode = 500;  //设置状态码
            response.setHeader("Content-Type","text/plain");  //设置返回的数据类型
            response.end("Sorry Internal Error");  //设置返回的数据
        }else{
            response.statusCode = statusCode;
            response.setHeader("Content-Type","text/html");
            response.end(data);
        }
    });
};

const server = http.createServer((request,response) =>{  // 使用模块创建NodeJS后端服务器
    console.log(request.url,request.method);
    const method = request.method;
    const url = request.url;
    if(method === 'GET'){
        if(url==='/'){  //访问根目录
            sendResponse('index.html',200,response);
        }else if(url === '/about.html'){
            sendResponse('about.html',200,response);
        }
        else{
            sendResponse('404.html',404,response);
        }
    }
    else
    response.end("Hello From NodeJS Server 2");
});

const port = 3000;
const ip = "127.0.0.1";

server.listen(port,ip,()=>{  
    console.log(`Server is running at http://${ip}:${port}`);  //es6模板字符串使用使${} 来包裹一个变量或者一个表达式,需要使用反引号 `
});  //服务器在运行时监听端口3000,从这个端口收集来自客户端的请求

Pasted image 20211030021706.png Pasted image 20211030021551.png


🌏 处理客户端发过来的GET请求参数

系统在网址后面加了一些字符串,我们来拆解一下这个网址,以 ? 作为分界线
Pasted image 20211030022102.png

amazon.com/s 是网址,? 右边是 GET Query String ,也就是 GET请求参数

GET请求参数里,第一个get参数是k,也就是我们搜索的关键字,第二个参数是 ref

手动更改 k 参数的值,按回车也会变


🍔 处理GET请求参数,根据参数切换页面语言

获取GET请求参数

Pasted image 20211030022811.png

💡 使用 JavaScript Url Object 获取Get请求参数

Url Object 需要两个参数(当前访问的页面 URL,基本URL IP+PORT)


Pasted image 20211030023111.png

⚠️ 显示成这样的解决办法,打开运行配置 launch.json

Pasted image 20211030024309.png
Pasted image 20211030024348.png

修改成这样,再次运行后端程序,切换到终端,即可看到简洁输出

Pasted image 20211030024542.png

⭐ 可以看到 searchParams 里有我们的请求参数,使用 get 方法获取参数
Pasted image 20211030024821.png
Pasted image 20211030024805.png


😢 此时出现了问题,不管访问哪个页面,只要后面添加lang=zh,都会定向到404页面,这是因为URL判断的部分出现了问题( url 常量值包含了请求参数 )
💡 我们可以用 JavaScript Url Object 的 pathname 属性来替代 url 常量

const http = require("http"); //导入http模块
const fs = require('fs');  //导入fs模块

const port = 3000;
const ip = "127.0.0.1";

const sendResponse = (filename,statusCode,response) => {
    fs.readFile(`F:/NodeJS/Study/common/1-2/html/${filename}`,(error,data)=>{
        console.log(error);
        if(error){
            response.statusCode = 500;  //设置状态码
            response.setHeader("Content-Type","text/plain");  //设置返回的数据类型
            response.end("Sorry Internal Error");  //设置返回的数据
        }else{
            response.statusCode = statusCode;
            response.setHeader("Content-Type","text/html");
            response.end(data);
        }
    });
};

const server = http.createServer((request,response) =>{  // 使用模块创建NodeJS后端服务器
    //console.log(request.url,request.method);
    const method = request.method;
    let url = request.url;
    if(method === 'GET'){
        const requestUrl = new URL(url,`http://${ip}:${port}`);
        console.log(requestUrl);
        console.log(requestUrl.searchParams.get("lang"));
        url = requestUrl.pathname;
        const lang = requestUrl.searchParams.get("lang");
        console.log(lang);
        if(url==='/'){  //访问根目录
            sendResponse(lang==='zh'?'index-zh.html':'index.html',200,response); // 或用动态字符串 `index${selector}.html`
        }else if(url === '/about.html'){
            sendResponse(lang==='zh'?'about-zh.html':'about.html',200,response);
        }
        else{
            sendResponse(lang==='zh'?'404-zh.html':'404.html',404,response);
        }
    }
    else
    response.end("Hello From NodeJS Server 2");
});

server.listen(port,ip,()=>{  
    console.log(`Server is running at http://${ip}:${port}`);  //es6模板字符串使用使${} 来包裹一个变量或者一个表达式,需要使用反引号 `
});  //服务器在运行时监听端口3000,从这个端口收集来自客户端的请求

Pasted image 20211030030216.png Pasted image 20211030030455.png


实操登录表单提交和POST后端处理

POST 请求一般用来向后端提交数据,像我们提交登录信息或创建新账号

客户端通过HTTP POST请求把数据传给后端,后端从POST请求里提取请求体Body的数据(除了能从表单里提交数据,POST请求也能提交JSON、XML数据、文件)


🍔 制作简易版的登录页面加后端处理,如果登录成功就跳转至成功页面,登录失败就跳转至失败页面

前端的数据是怎么传递到后端的?


  • 把互联网想象成大水管,数据像水一样从水管流向后端服务器,Stream

  • 前端的数据在进入水管前会被转换成 Binary Data 然后流向后端服务器

  • 在传递过程中,数据源会被分成好几个部分然后分批发送

  • 最后 NodeJS 在另一方分批的接收数据

  • 当数据分批抵达 NodeJS 服务器的时候,这些数据会先存储在一个缓冲区 Buffer

  • NodeJS 会根据情况把缓冲区的数据传送到程序里,让程序开始读取

  • 💡 缓冲区的好处:可以控制数据流的速度,当数据流流向后端程序的速度超过程序读取的速度,这些来不及读取的数据能够先存放在缓存区里等待程序读取;如果数据流来的慢,数据也需要被保存在缓存区里,直到达到一定的量再被读取,所以后端程序能够有序进行(YOUTUBE播放器)



编写POST请求的代码

思路步骤:


  • ⭐ 程序是分批读取缓冲区里的数据,所以我们需要创建一个Array,每读取一次,就把刚读取的数据保存在Array里,最后我们把Array的分段数据合并,变成完整的数据

  • ⭐ 我们需要插入两个监听器,一个用来监听缓冲区里的数据能否读取,另一个用来监听数据是否全部读取

    Pasted image 20211030040728.png
    Pasted image 20211030040741.png
    Pasted image 20211030040747.png



🍔 后端已经拿到了字符串,怎么进行拆分


  • 💡 我们需要导入 QueryString 模块,使用 parse 函数
    Pasted image 20211030041105.png



Pasted image 20211030041404.png
可以看到Parse函数返回了一个Object,接下来我们就可以进行判断

要让前端跳转到其他页面,我们需要将状态码设置成301
Pasted image 20211030042342.png
Pasted image 20211030042348.png
Pasted image 20211030042400.png

const http = require("http"); //导入http模块
const fs = require('fs');  //导入fs模块
const qs = require('querystring');

const port = 3000;
const ip = "127.0.0.1";

const sendResponse = (filename,statusCode,response) => {
    fs.readFile(`F:/NodeJS/Study/common/1-3/html/${filename}`,(error,data)=>{
        //console.log(error);
        if(error){
            response.statusCode = 500;  //设置状态码
            response.setHeader("Content-Type","text/plain");  //设置返回的数据类型
            response.end("Sorry Internal Error");  //设置返回的数据
        }else{
            response.statusCode = statusCode;
            response.setHeader("Content-Type","text/html");
            response.end(data);
        }
    });
};

const server = http.createServer((request,response) =>{  // 使用模块创建NodeJS后端服务器
    console.log(request.url,request.method);
    const method = request.method;
    let url = request.url;
    if(method === 'GET'){
        const requestUrl = new URL(url,`http://${ip}:${port}`);
        url = requestUrl.pathname;

        if(url==='/'){  //访问根目录
            sendResponse('index.html',200,response); // 或用动态字符串 `index${selector}.html`
        }
        else if(url === '/login.html'){
            sendResponse('login.html',200,response);

        }
        else if(url === '/login-success.html'){
            sendResponse('login-success.html',200,response);
        }
        else if(url === '/login-fail.html'){
            sendResponse('login-fail.html',200,response);
        }
        else{
            sendResponse('404.html',404,response);
        }
    }
    else{
        if(url==='/process-login'){
            let body = [];
            request.on("data",(chunk)=>{  //监听 data ,chunk是分段读取的数据
                body.push(chunk);
            });

            request.on("end",()=>{  // 监听 end 
            body = Buffer.concat(body).toString(); // 将分段的Buffer数据合并变成完整的Buffer数据
            body = qs.parse(body); // 使用 qs 模块的 parse 函数将 body 字符串拆分
            console.log(body);
            if(body.username==='admin' && body.password==='123456'){
                response.statusCode = 301;
                response.setHeader("Location","/login-success.html");
            }else{
                response.statusCode = 301;
                response.setHeader("Location","/login-fail.html");
            }
            response.end();  //发送响应
            })
        }
    }

});

server.listen(port,ip,()=>{  
    console.log(`Server is running at http://${ip}:${port}`);  //es6模板字符串使用使${} 来包裹一个变量或者一个表达式,需要使用反引号 `
});  //服务器在运行时监听端口3000,从这个端口收集来自客户端的请求

⭐ 什么是NPM

中文名:包管理工具 模块管理工具(NodeJS Package Manager)

两个重要功能:模块仓库,Command Line Interface CLI命令界面

⭐ 模块仓库 Registry

Pasted image 20211030042817.png
内置核心模块可以直接在代码里导入和调用

Pasted image 20211030042932.png

⭐ NPM CLI
输入 npm -v 查看版本

演示安装 express 服务器模块


  • cd 到工作目录里

  • npm init 初始化工作目录(生成一个package.json储存的项目信息)

  • npm install express (可以简化成 npm i express)
    安装成功后,我们可以在代码里导入调用模块
    Pasted image 20211030043246.png

⚠️ 模块保存在 node_modules 文件夹里,因为express模块它自己也依赖其他的模块,所以目录下不止 express 这一个模块文件夹

卸载express模块


  • nom uninstall express



程序员怎么自定义模块

为什么要模块?模块化源代码能给我们带来什么好处?

试想一个巨无霸网购平台,在没有模块化的情况下,如果出现bug,程序员就要在几百万行代码里调试,导致后期维护成本上升,为了解决问题,模块化按功能切分,把大问题转换成小问题,让每个模块独立运营,通过接口对外开放,让程序统一调用,降低程序出错的风险,也能方便升级模块内部的代码,不影响全局

Pasted image 20211030044120.png

创建模块(🍔简易版计算机)

模拟巨无霸程序(现有两个js文件 app.js server.js)
Pasted image 20211030044643.png

如果没有模块化程序,那么server.js也想用这些功能的时候也必须将函数写一遍。就会出现代码重叠,如果要修改,就要全部进行修改
⭐ 所以我们将这部分函数模块化


  • 新建calculator.js,将 app.js 的函数剪贴到其中(此时再运行app.js会出错)

  • 我们需要在 app.js 中引入我们新建的模块
    Pasted image 20211030045210.png
    打印出来了一个 { } 空 Object 对象,⚠️ 这是因为 calculator.js 里的函数是不会自动暴露或者被输出的,我们需要明确告诉NodeJS哪一个函数需要被输出,那样才能被其他程序引入和调用





  • 模块使用 module.exports 语句,在运行 app.js
    Pasted image 20211030045407.png
    Pasted image 20211030045420.png
    console输出函数 add





  • 😅 那 subtract 怎么办呢,因为module.exports={ },我们可以往里添加新数据,我们只需要提供一对 key-value 即可
    Pasted image 20211030045629.png
    Pasted image 20211030045654.png
    console输出两个函数 add, subtract



`js
// app.js
const calculator = require('./calculator.js');

console.log(calculator);

let addResult = calculator.add(1,2);

console.log(1 + 2 = ${addResult});

 Pasted image 20211030045913.png 
```js
const calculator = require('./calculator.js');

console.log(calculator);

let SubtractResult = calculator.subtract(5,2);

console.log(`5 - 2 = ${SubtractResult}`);

Pasted image 20211030045852.png

function add(num1,num2){
    return num1 + num2;
}

function subtract(num1,num2){
    return num1-num2;
}

//module.exports.add = add;
//module.exports.subtract = subtract;

//exports.add = add;
//exports.subtract = subtract;

module.exports = {
    add,  //add:add,
    subtract     //subtract:subtract
}

💡 自定义模块小技巧


  • ⭐ 如果我们有很多模块,重复输入 module.exports 是很麻烦的,NodeJS给我们提供了一个便利,直接把最前面的 module 去掉

  • ⭐ 我们可以写成 module.exportsz = { 内容 } 的形式来简化,如果key-value相等,可以只写key
    Pasted image 20211030050257.png


⭐NodeJS 连接Mysql数据库 ①

GitHub mysqljs A pure node.js JavaScript Client implementing the MySQL protocol.

// 加载模块
let mysql = require('mysql');

// 数据库连接配置
let conn = mysql.createConnection({
    host: 'localhost',
    user: 'username',
    password: 'yourpassword',
    database: 'mydb'
});

// 连接数据
conn.connect();

// 查询用户表数据
conn.query('SELECT * FROM user;', function(err, res, rows){
    if(err){
        // 异步处理
        throw err;
    }else{
        // 查询成功,处理数据
        console.log(rows);
    }
});

// 关闭数据库连接
// 数据不关闭会造成连接池过大,导致事故哦
conn.end();
const mysql  = require('mysql');

let connection = mysql.createConnection({  //创建数据库连接应用
  host     : 'localhost',
  user     : 'root',
  password : 'mysql2008',
  database : 'kms2020',
  port : 3306
});

connection.connect(); //连接数据库

connection.query('call d2004_1();', function (error, results, fields) {  //执行存储过程,执行完毕调用回调函数
    if (error) throw error;
      console.log(results[0][0]);
});

connection.end();

Pasted image 20211030051728.png

⚠️ 每次数据查询都需要按顺序排队执行

⚠️ 每次数据查询之后必须关闭连接池,以确保其它排队成员可以正常查询

关闭连接

两种方法


  • end( ) 方法,有回调函数,参数error

  • destory( )方法,无回调函数


🍔 尝试编写RunSqlProcedure

# MYSQL 三个存储过程
use mysales;

drop procedure if exists sp1;
delimiter $$
create procedure sp1(
)
begin
select * from products;
end $$
delimiter ;

call sp1();

drop procedure if exists sp2;
delimiter $$
create procedure sp2(
$productid int
)
begin
select * from products where productid = $productid;
end $$
delimiter ;

call sp2(10);

drop procedure if exists sp3;
delimiter $$
create procedure sp3(
$categoryid varchar(1),
$unitprice decimal(8,2)
)
begin
select * from products where categoryid = $categoryid and unitprice>$unitprice;
end $$
delimiter ;

call sp3('A',30);
// mysql 模块化

const mysql = require("mysql");
const { callbackify } = require("util");
const { resourceLimits } = require("worker_threads");

const myDatabase = 'mysales';

const con = mysql.createConnection({
    host: 'localhost',
    user: 'root',
    port: 3306,
    password: 'mysql2008',
    database:myDatabase
})

con.connect();

function myRunSqlProcedure(procedure,obj,callback){
    //console.log(procedure,obj);
    con.query(`SHOW PROCEDURE STATUS where name = "${procedure}" and db = "${myDatabase}";`,(err,res,fie)=>{
        if(err) throw err;
        if(res.length==0){
            console.log(`存储过程${procedure}不存在!`);
            return;
        }
        MySetParams(procedure,obj,(command)=>{
            //回调函数
            con.query(command,(err,res,fie)=>{
                if(err) throw err;
                var a = {};
                a.rows = res[0];
                a.status = res[1];
                callback(a);
            })
        });
    });
}

function MySetParams(procedure,obj,myFun){
    let command = `call ${procedure}(`;
    // 进行参数拼接
    let params = `SHOW CREATE PROCEDURE ${myDatabase}.${procedure};`
    //console.log(params);
    con.query(params,(err,res,fie)=>{
        if(err) throw err;
        params = res[0]['Create Procedure'];
        params = params.split('begin')[0].split('$');
        let count = params.length-1;
        let IsSameParams = count == Object.keys(obj).length;
        //console.log(count,Object.keys(obj).length,count == Object.keys(obj).length);
        if(IsSameParams == false) throw '参数不一致';

        if(count!=0)
            for(var i = 0;i<count;i++){
            const procedureKey =params[i+1].split(' ')[0];
            for(var key in obj){
                //console.log(key,procedureKey);
                if(key.toLowerCase() == procedureKey.toLowerCase()) {
                    if(typeof(obj[key])=='string')
                    command += '\''+obj[key]+'\'';
                    else 
                    command += '\''+obj[key]+'\'';
                }
            }
            if(count>1&&count!=i+1) command += ',';
        }
        command += ');'
        myFun(command);  // 可调用回调函数
    })
}

function Close(){
    con.end();
}


module.exports ={
    myRunSqlProcedure,
    Close
}

Pasted image 20211030071301.png

Pasted image 20211030071312.png

😅 问题:主函数拿不到异步回调函数的值,只能传方法

😜 Node.js 学习笔记 2

🖥️ CMD终端的基本使用

sysdm.cpl  可快速打开计算机设置配置环境变量
md abc     创建目录 abc
rd abc     删除目录 abc
cd ..    进到上层目录 

如果目录不是空的不能用rd删除目录,需要加两个参数
rd /s/q abc   (sub-directory quiet)

echo on a.txt 创建 a.txt
往里面覆盖内容
echo 123 > a.txt
往里面追加内容
echo 456 >> a.txt

cat a.txt 输出文件内容

rm a.txt 删除文件

cat > hello.txt 覆盖(按ctrl c 退出)
cat >> hello.txt 追加内容(按ctrl c 退出)

在文件内按 Shift 右键 选择 在该目录下执行命令行

全局成员初步认识(1-1)

console.log(123);

// 包含文名称的全路径
console.log(__filename);
// 文件的路径(不包含文件名称)
console.log(__dirname);

// 定时函数
const a = setTimeout(()=>{
    console.log(123);
},3000);

setTimeout(()=>{console.log(123)},200);
clearTimeout(a);

// process.argv 是一个数组,默认情况前两项数据分别是:Node.js的安装环境的路径,当前执行的js文件的全路径
// 从第三个参数开始表示命令行参数
console.log(process.argv);
console.log(process.argv0);

// 打印当前系统的架构(x64, x86)
console.log(process.arch);

初识模块化(1-2)

/*
    index.js
    模块化开发

    传统非模块化开发有如下的缺点:
    1、命名冲突(团队开发)
    2、文件依赖(文件引入与先后次序)

    前端标准的模块化规范:
    1、AMD - requirejs
    2、CMD - seajs

    后端标准的模块化规范:
    1、CommonJS - Node.js

    模块化相关的规则:
    1、 如何定义模块:一个js文件就是一个模块,模块内部的成员都是相互独立的
    2、 模块成员的导出和引入
*/

const a = function(a,b){
    return parseInt(a) + parseInt(b);
}

// 导出模块成员
//exports.sum = a;

module.exports = a; //此时这个模块本身就是个方法
/*
    a.js
    引入模块
*/

const mod = require('./index.js');

console.log(mod);

//var ret = mod.sum(12,13);

var ret = mod(12,13);

console.log(ret);

模块化细节补充

/*
    index.js
    模块成员的导出 global

    已经加载的模块会缓存,如果内存中有同样的文件就不会加载(性能优化)
*/

console.log('hello');

var flag = 123;

// 很少使用
global.flag = flag;
/*
    a.js
    模块文件的后缀3种情况: .js .json .node(C、C++)(不加后缀时加载顺序)
    */

//require('./index.js'); 
//require('./index.js');
//require('./index.js');

require('./index');   // 后缀可以直接省略

var data = require('./data')

console.log(global.flag);
console.log(data);
// data.json
{
    "username":"张三",
    "age":12
}

ES6 let、const 使用规范

/*
    声明变量 let和const
*/

// let 声明的变量不存在预解析
//console.log(flag);
// var flag = 123;
// let flag = 456;

// let 声明的变量不允许重复(同一作用域)
//let flag = 123;
//let flag = 456;
//console.log(flag);

// ES6 引入了块级作用域
// 块内部let定义的变量,在外部访问不到
// if(true){
//     //var flag = 123;
//     let flag = 123;
// }
/* -------------------------------------------------------------------------- */
{
    // 这里是块级作用域
    let flag = 123;
    console.log(flag);
}
//console.log(flag);
/* -------------------------------------------------------------------------- */
for(let i = 0; i < 3; i++){
    // for循环括号中let声明的变量只能在循环体中使用
    console.log(i);
}
/* -------------------------------------------------------------------------- */// 在块级作用域内部,变量只能先声明再使用
// 在块级作用域内部,变量只能先声明再使用
if(true){
    //console.log(flag);
    let flag = 123;
}

/* ---------------------------------- const --------------------------------- */

// const用来声明常量,不允许重新赋值,一开始必须要初始化
// 上述规则对 const 同样适用
// const n = 1;
// n = 2;

ES6 变量解构赋值

/*
    变量的解构赋值
*/

// var a = 1;
// var b = 2;
// var c = 3;

// var a = 1,b = 2,c = 3;

// * 数组的解构赋值,根据顺序,还可以添加默认值
//let [a,b,c] = [1,2,3];

//let [a=111,b,c] = [,123,];

//console.log(a,b,c);

// * 对象的解构赋值
//let {foo,bar} = {foo:'hello',bar : 'hi'};
//let {foo,bar} = {bar : 'hi',foo:'hello'};

// * 对象属性别名(如果起了别名,那么原来的名字就无效了)
// let {foo:abc,bar} = {foo:'hello',bar : 'hi'}

// console.log(typeof(abc),bar,abc);

// * 对象的解构赋值设置默认值
let {foo:abc="hello",bar} = {bar : 'hi'};
console.log(abc,bar);

// * 把对象当中的属性与名称的进行绑定
let {cos,sin,random} = Math;

console.log(typeof cos,typeof sin,typeof random);
/* -------------------------------------------------------------------------- */
// * 字符串的解构赋值

let [a,b,c,d,e,length] = "hello";
console.log(a,b,c,d,e,length);

let {length:len} = "hel";

console.log(len);

ES6 字符串扩展与模板字符串

/*
    字符串相关扩展
    includes()  判断字符串中是否包含指定的子串(有的话返回true,否则返回false)
                参数一:匹配的子串;参数二:从第几个字符开始匹配
    startsWith()  判断字符串是否以特定的子串开始
    endsWith()  判断字符串是否以特定的子串结束

    模板字符串
*/

console.log('hello world'.includes('world',6));

let url = 'admin/index.php';

console.log(url.startsWith('admin'));
console.log(url.endsWith('php'));

/* -------------------------------------------------------------------------- */
let obj = {
    username : '李四',
    age:'12',
    gender: 'male'
};

let tag = '<div><span>'+obj.username+'</span><span>'+obj.age+'</span><span>'+obj.gender+'</span></div>';
// 反引号表示模板,模板中的内容可以有格式,通过 ${}方式填充数据
let fn = (x)=> x;
let tag2 = `
<div>
    <span>${obj.username}</span>
    <span>${obj.age}</span>
    <span>${obj.gender}</span>
    <span>${1+1}</span>
    <span>${fn('nihao')}</span>
</div>
`

console.log(tag);
console.log(tag2); // * 模板字符串后期维护方便

ES6 函数扩展

/*
    函数扩展
    - 参数默认值
    - 参数解构赋值
    - rest参数
    - ...扩展运算符
*/

//参数默认值老式方法
function foo(param) {
    let p = param || 'hello';
    console.log(p);
}
foo();

function foo2(param = 'nihao') {
    console.log(param);
}

foo2();
foo2('123456');

/* -------------------------------------------------------------------------- */

function foo3(uname = 'list', age = 12) {
    console.log(uname, age);
}

foo3();
foo3('张三', 13);

// * 参数解构赋值
function foo4({
    uname = 'lisi',
    age = 10
} = {}) {
    console.log(uname, age);
}

foo4(); //! 不传参会出问题,可以给函数参数一个默认值 {}
foo4({
    uname: 'zhangsan',
    age: 15
});

/* -------------------------------------------------------------------------- */
// * rest参数(剩余参数)...
function foo5(a, ...params) {
    console.log(params);
}

foo5(1, 2, 3, 4, 5);

// 扩展运算符 ...

function foo6(a, b, c, d, e, f){
    console.log(a + b + c + d + e + f);
}

foo6(1,2,3,4,5,6);
let arr = [1,2,3,4,5,6];
//foo6.apply(null,arr); 

foo6(...arr);  // ! 扩展运算符

// 合并数组
let arr1 = [1,2,3];
let arr2 = [4,5,6];
let arr3 = [...arr1,...arr2];
console.log(arr3);

apply、call函数妙用

ES6 箭头函数

Pasted image 20211031150146.png

node执行调试完成之后进程退出,这时候去看它的打印内容就看不到了

需要看到他具体的值,在打印的地方加上断点即可
/*
    箭头函数
*/

function foo(){
    console.log('hello');
}
foo();

let foo2= () => console.log('hello');
foo2();

let foo3= x => x;
foo3(123);

// * 多个参数必须用小括号包住
let foo4 = (a,b) => console.log(a+b);
foo4(1,2);

let arr = [1,2,3];
arr.forEach(function(value,index){
    console.log(value,index);
});

arr.forEach((value,index)=>console.log(value,index));

// ! 箭头函数的注意事项
// - 箭头函数中的this取决于函数的定义而不是调用
// - 箭头函数不可以 new
// - 箭头函数不可以使用 arguments 获取参数列表,可以使用rest参数代替
function foo5(){
    // 使用call调用foo5时,这里的this其实就是call的第一个参数
    console.log(this);
    setTimeout(()=>{
        console.log(this,this.num);
    },1000);
}

foo5();
foo5.call({num:1});
/* -------------------------------------------------------------------------- */
let foo6 = ()=> {this.num = 123;}
//new foo6();
/* -------------------------------------------------------------------------- */
let foo7 = (a,b) =>{
    console.log(a,b,arguments); //这种方式获取不到实参列表
}
foo7(123,456);

let foo8 = (...param) => {  //rest参数
    console.log(param);
}
foo8(123,456);

ES6 类与继承

/*
    类与继承
*/
// function Animal(name){
//     this.name = name;
// }

// Animal.prototype.showName = function(){
//     console.log(this.name);
// }
// var a = new Animal('Tom');
// a.showName();
// var b = new Animal('Jerry');
// a.showName();
/* -------------------------------------------------------------------------- */

class Animal{
    // * 静态方法(静态方法只能通过类名调用,不可以使用实例对象调用)
    static showInfo(){
        console.log('hello');
    }
    // * 构造函数
    constructor(name){
        this.name = name;
    }
    showName(){
        console.log(this.name);
    }
}
let a = new Animal('spike');
a.showName();
Animal.showInfo();
/* -------------------------------------------------------------------------- */
// 类的继承 extends
class Dog extends Animal{
    constructor(name,color){
        super(name); // super用来调用父类
        this.color = color;
    }
    ShowColor(){
        console.log(this.color);
    }
}

let d = new Dog('doudou','yellow');
d.showName();
d.ShowColor();
Dog.showInfo();

Buffer基本操作

console.log(Buffer.isEncoding('utf8'));
console.log(Buffer.isEncoding('gbk'));

let buf = Buffer.from('hello');
console.log(buf);
console.log(Buffer.isBuffer(buf));
console.log(Buffer.isBuffer({}));

let buf2 = Buffer.from('中国','ascii');
console.log(Buffer.byteLength(buf2));
console.log(buf2.toString());

let buf31 = Buffer.alloc(3);
let buf32 = Buffer.alloc(5);
let buf33 = Buffer.concat([buf31,buf32]);
console.log(Buffer.byteLength(buf33));

/* -------------------------------------------------------------------------- */
// 实例方法 

let buf4 = Buffer.alloc(5);
buf4.write('hello',2,2); // * 向buffer对象中写入内容  [from] - counts
console.log(buf4);

let buf51 = Buffer.from('hello');
let buf52 = buf51.slice();
console.log(buf51==buf52); // ! 两个不同的buffer对象
buf52 = buf51.slice(1,3); // * 截取buffer对象的内容 [from] - (to)
console.log(buf52.toString());

// ! toJSON方法不需要显式调用,当JSON.stringify方法调用的时候会自动调用toJSON方法 
const buf6 = Buffer.from('hello');
const json = JSON.stringify(buf6); // * 将buffer对象转成JSON,对应的十进制
console.log(json);

路径操作

/*
    路径操作
*/
const path = require('path');

// * 获取路径的最后一部分
console.log(path.basename('/foo/bar/baz/asdf/quux.html'));
console.log(path.basename('/foo/bar/baz/asdf/quux.html','.html'));

// * 获取路径
console.log(__dirname);
console.log(path.dirname('/abc/qq/www/aabc.txt'));

// * 获取文件扩展名
console.log(path.extname('index.html'));

// * 路径的格式化处理
// path.format() obj->string
// path.parse() string->obj

let obj = path.parse(__filename);
console.log(obj);

/*
{
  root: 'F:\\',   文件根路径
  dir: 'F:\\Node.JS\\Codes\\1031\\NodeJS基础\\2-1 路径操作', 文件全路径
  base: 'index.js', 文件名称
  ext: '.js',   文件扩展名
  name: 'index' 文件名称
}
*/

let objpath = {
    root: 'd:\\',  
    base: 'abc.txt',
    dir : 'd:\\aaaaa\\cccc\\', 
    ext: '.txt',   
    name: 'abc', 
};
let strPath = path.format(objpath);
console.log(strPath);

// * 判断是否为绝对路径
path.isAbsolute('C:/foo/..'); 

// * 拼接路径,在连接路径的时候会格式化
console.log(path.join('/foo','bar','baz/asdf','quux','../../')); // ! 两个点是上层路径,一个点表示当前路径

// * 规范化路径
console.log(path.normalize('/foo/bar//baz/asdf/quux/..'))
console.log(path.normalize('C:\\temp\\\\foo\\bar\\..\\'));

// * 从两个绝对路径换算出来相对路径
console.log(path.relative('C:\\orandea\\test\\aaa', 'C:\\orandea\\impl\\bbb'));

// * 解析路径
console.log(path.resolve('wwwroot', 'static_files/png/', '../gif/image.gif'));

// * 两个特殊属性
console.log(path.delimiter); // * 环境变量分隔符 windows 是 ; linux 是 :
console.log(path.sep);  // * 路径分隔符,windows 是 \  LINUX是 /

异步I/O

Pasted image 20211101153148.png

/*
    异步I/O input/output
    - 文件操作
    - 网络操作

    在浏览器中也存在异步操作:
    - 定时任务
    - 事件处理
    - Ajax回调处理

    js的运行是单线程的
    引入了事件队列机制

    Node.js中的事件模型与浏览器中的事件模型类似
    单线程+事件队列

    Node.js中异步执行的任务:
    1、文件I/O
    2、网络I/O

    基于回调函数的编码风格
*/

文件操作

文件状态

/*
    文件状态
*/
const fs = require('fs');
console.log(1);
fs.stat(`${__dirname}/data.txt`,(err,stat)=>{
    // ! 一般回调函数的第一个参数是错误对象,如果err为null,表示没有错误,否则表示报错了
    if(err) return;
    console.log(stat);
    if(stat.isFile()){
        console.log('文件');
    }
    else if(stat.isDirectory()){
        console.log('目录');
    }
    /* 
    atime 访问时间
    mtime 文件数据发生变化的时间
    ctime 文件状态信息发生变换的时间(比如文件的权限)
    birthtime 文件创建的时间
    */
});
console.log(2);

// 同步操作
// console.log(1);
// let ret = fs.statSync(`${__dirname}/data.txt`);
// console.log(ret);
// console.log(2);

// ! 当主线程执行完了才会空闲,去把事件队列中的任务取出来

读文件

/*
    读文件操作
*/
const fs = require('fs');
const path = require('path');

let strPath = path.join(__dirname,'data.txt');

fs.readFile(strPath,(err,data)=>{
    if(err) return;
    console.log(data); // * 打印出字节数组
    console.log(data.toString());
});

// * 第二个参数可以指定编码,得到的数据是字符串
// * 如果没有第二个参数,那么得到的就是Buffer实例对象
fs.readFile(strPath,'utf8',(err,data)=>{
    if(err) return;
    console.log(data); // * 打印出字符串
});

// ! 同步操作,不需要回调函数
let ret = fs.readFileSync(strPath,'utf8');
console.log(ret);

写文件

/*
    写文件操作
*/
const fs = require('fs');
const path = require('path');

let strpath = path.join(__dirname,'data.txt');
// * 默认编码方式 encoding : utf 8
// fs.writeFile(strpath,'hello cat',(err)=>{
//     if(err) throw err;
//     else {
//         console.log('文件写入成功');
//     }
// })
// ! 多次写入需要使用数据流的方式

// let buf = Buffer.from('world');
// fs.writeFile(strpath,buf,(err)=>{
//     if(err) throw err;
//     else {
//         console.log('文件写入成功');
//     }
// })

// ! 同步操作
fs.writeFileSync(strpath,'tom and jerry');

文件流式操作(针对大文件)

/*
    大文件操作(流式操作)
    fs.createWriteStream(path[, options])
    fs.createReadStream(path[, options])
*/
const path = require('path');
const fs = require('fs');

let sPath = path.join(__dirname,'../files','file.zip');
let dPath = path.join('P:\\','file.zip');

let readStream = fs.createReadStream(sPath);
let writeStream = fs.createWriteStream(dPath);

// 基于事件的处理方式
// * 举例
// $('input[type=button]').on('click',function(){
//     console.log('hello');
// });

// * 在NodeJS中无DOM操作,所以没有点击事件

// let num = 0;
// readStream.on('data',(chunk)=>{ // ! data 是固定的事件,每读取一部分就触发
//     num++;
//     writeStream.write(chunk);
// });

// readStream.on('end',()=>{ // ! end 是固定的事件
//     console.log(num);
//     console.log('文件处理完成');
// })

/* -------------------------------------------------------------------------- */

// * 另外一种方式,pipe的作用直接把输入流(从磁盘加载到内存)与输出流(从内存写入到磁盘)
readStream.pipe(writeStream);

/* -------------------------------------------------------------------------- */

fs.createReadStream(sPath).pipe(fs.createWriteStream(dPath));

目录操作

/*
    目录操作
    - 创建目录
    fs.mkdir(path[, options], callback)
    fs.mkdirSync(path[, options])
    - 读取目录
    fs.readdir
    fs.readdirSync

*/

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

// fs.mkdir(path.join(__dirname,'../files','abc'),(err)=>{
//     console.log(err);
// });

// fs.mkdirSync(path.join(__dirname,'../files','abc'));
/* -------------------------------------------------------------------------- */
// 读取目录下的目录与文件
// fs.readdir(path.join(__dirname,'..'),(err,files)=>{
// console.log(err,files);
//     files.forEach((item,index)=>{
//         fs.stat(path.join(__dirname,'..',item),(err,stat)=>{
//             if(stat.isFile()){
//                 console.log(item,'文件');
//             }
//             else if(stat.isDirectory()){
//                 console.log(item,'目录');
//             }
//         })
//     })
// });

// ! 同步操作
// let files = fs.readdirSync(path.join(__dirname,'..'));
// files.forEach((item,index)=>{
//     fs.stat(path.join(__dirname,'..',item),(err,stat)=>{
//         if(stat.isFile()){
//             console.log(item,'文件');
//         }
//         else if(stat.isDirectory()){
//             console.log(item,'目录');
//         }
//     })
// })

// ! 删除目录
fs.rmdir(path.join(__dirname,'../files','abc'),(err)=>{
    console.log(err);
});

fs.rmdirSync(path.join(__dirname,'../files','abc'));

文件实操案例(初始化目录与文件)

/*
    文件操作案例(初始化目录结构)
*/

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

let root = 'P:\\'
let fileContent = `
<!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>
</head>
<body>
    欢迎访问
</body>
</html>
`;

// 初始化数据
let initData = {
    projectName : 'mydemo',
    data : [{
        name : 'img',
        type : 'dir'
    },{
        name : 'js',
        type : 'dir'
    },{
        name : 'css',
        type : 'dir'
    },{
        name : 'index.html',
        type : 'file'
    }]
};

// 创建项目根路径
fs.mkdir(path.join(root,initData.projectName),(err)=>{
    if(err) return;
    // 创建子目录和文件
    initData.data.forEach((item)=>{
        if(item.type == 'dir'){
            // * 创建子目录
            fs.mkdirSync(path.join(root,initData.projectName,item.name));
        }
        else if(item.type == 'file'){
            fs.writeFileSync(path.join(root,initData.projectName,item.name),fileContent);
        }
    })
});

😜 Node.js 学习笔记 3

NPM

NPM 安装与卸载包

/*
    全局安装 -g:
    全局安装的包位于Node.js环境的node_modules目录下,全局安装的包一般用于命令行工具

    本地安装:
    本地安装的包在当前目录下的node_modules里面,本地安装的包一般用于实际的开发工作
    ------------------------------------------------------------
    npm常用的命令:
    1、安装包(如果没有指定版本号,那么安装最新版本)
    npm install -g 包名称 (全局安装)
    npm install 包名称 (本地安装)

    2、安装包的时候可以指定版本
    npm install -g 包名称@版本号

    3、卸载包
    npm uninstall -g 包名

    4、更新包(更新到最新版本)
    npm update -g 包名

    开发环境(平时开发使用的环境)
    生产环境(项目部署上线之后的服务器环境)
    --save 向生产环境添加依赖 dependencies
    --save-dev 向开发环境添加依赖 DevDependencies 

*/

NPM

Pasted image 20211102073655.png

每次用到终端都非常的难受。默认的路径都是在 home 目录,而不是当前工作文件的位置

搜索 **openInTerminal**,给它绑定一个快捷键即可
/*
    yarn工具基本使用

    安装yarn工具:npm install -g yarn

    1、初始化包
    npm init
    yarn init
    2、安装包
    npm install xxx --save
    yarn add xxx
    3、移除包
    npm uninstall xxx
    yarn remove xxx
    4、更新包
    npm update xxx
    yarn upgrade xxx
    5、安装开发依赖的包
    npm install xxx --save-dev
    yarn add xxx --dev
    6、全局安装
    npm install -g xxx
    yarn global add xxx
    7、设置下载镜像的地址
    npm config set registry url
    yarn config set registry url
    8、安装所有依赖
    npm install
    yarn install
    9、执行包
    npm run
    yarn run
*/

自定义包(markdown转html)

/* 
    入口文件
*/

const path = require('path');
const fs = require('fs');
const md = require('markdown-it')();

let tplPath = path.join(__dirname,'tpl.html');
let mdPath = path.join(__dirname,'demo.md');
let targetPath = path.join(__dirname,'demo.html');

// var md = require('markdown-it')();
// var result = md.render('# markdown-it rulezz!');
// console.log(result);

// 获取markdown文件
fs.readFile(mdPath,'utf8',(err,data)=>{
    if(err) return;
    // 转化markdown为html语言
    let result = md.render(data);
    // 读取模板内容
    let tpl = fs.readFile(tplPath,'utf8',(err,tplData)=>{
        if(err) return;
        tplData = tplData.replace('<%content%>',result);
        // 生成最终页面写入目标文件
        fs.writeFile(targetPath,tplData,(err)=>{
            if(err) return;
            console.log('转化完成');
        });
    })
});


HTTP

初步实现服务器功能

/* 
    初步实现服务器功能
*/

const http = require('http');

// // ! 创建服务器实例对象
// let server = http.createServer();
// // ! 绑定请求事件
// server.on('request',(req,res)=>{
//     res.end('hello');
// });
// // ! 监听端口
// server.listen(3000);

http.createServer((req,res)=>{
    res.end('hello');
}).listen(3000,()=>{console.log('Running')});

处理路径分发

/* 
    处理路径的分发
    - req对象是Class http.IncomingMessage 实例对象
    - res对象是Class http.ServerResponse 实例对象
*/

const http = require('http');

http.createServer((req,res)=>{
    // ! req.url 可以获取URL中的路径(端口之后的部分)
    // res.end(req.url);
    if(req.url.startsWith('/index')){
        // ! write方法向客户端响应内容
        res.write('hello');
        res.write('hi');
        // ! end方法用来完成响应,只能执行一次
        res.end('index');
    }else if(req.url.startsWith('/about')){
        res.end('about');
    }else res.end('404');
}).listen(3000,()=>{console.log('Running')});

网页路径分发

Pasted image 20211104085445.png

/* 
    响应完整的页面信息
*/

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

let readFile = (filename,res)=>{
    fs.readFile(path.join(__dirname,'/html',filename),'utf8',(err,data)=>{
        if(err) res.end('server error');
        else res.end(data);
    })
}

http.createServer((req,res)=>{
    if(req.url.startsWith('/index')){
        readFile('index.html',res);
    }else if(req.url.startsWith('/about')){
        readFile('about.html',res);
    }else{
        res.writeHead(200,{
            'Content-Type':'text/plain; charset=utf8'
        })
        res.end('网页被狗狗叼走了');
    } 
}).listen(3000,()=>{
    console.log('Running');
});

网络路径分发 2

/* 
    响应完整的页面信息
*/

const http = require('http');
const path = require('path');
const fs = require('fs');
const mime = require('../mime.json');
const { Console } = require('console');

http.createServer((req, res) => {
    fs.readFile(path.join(__dirname, 'html', req.url), (err, data) => {
        if (err) {
            res.writeHead(404, {
                'Content-Type': 'text/plain; charset=utf8'
            });
            res.end('页面被狗狗叼走了');
        } else {
            let dtype = 'text/html';
            // ! 获取请求文件的后缀
            let ext = path.extname(req.url);
            // ! 如果请求的文件后缀合理,就获取到标准的响应格式
            if (mime[ext]) dtype = mime[ext];
            if (dtype.startsWith('text')) dtype += '; charset=utf8'
            // ! 设置头信息
            //console.log(dtype);
            res.writeHead(200,{'Content-Type':dtype});
            res.end(data);
        }
    });
}).listen(3000, () => {
    console.log('Running');
});

服务器模块化

// server.js
const path = require('path');
const fs = require('fs');
const mime = require('../mime.json');

exports.staticServer = (req,res,root) => {
        fs.readFile(path.join(root, req.url), (err, data) => {
            if (err) {
                res.writeHead(404, {
                    'Content-Type': 'text/plain; charset=utf8'
                });
                res.end('页面被狗狗叼走了');
            } else {
                let dtype = 'text/html';
                // ! 获取请求文件的后缀
                let ext = path.extname(req.url);
                // ! 如果请求的文件后缀合理,就获取到标准的响应格式
                if (mime[ext]) dtype = mime[ext];
                if (dtype.startsWith('text')) dtype += '; charset=utf8'
                // ! 设置头信息
                //console.log(dtype);
                res.writeHead(200,{'Content-Type':dtype});
                res.end(data);
            }
        }
     )};
//index.js
const http = require('http');
const ss = require('./server.js');
const path = require('path');

http.createServer((req,res)=>{
    ss.staticServer(req,res,path.join(__dirname,'html'));
}).listen(3000,()=>{
    console.log('success');
});

GET POST

//url
/* 
    get 参数处理
*/
const url = require('url');

let str = "http://www.baidu.com/abc?flag=123&keyword=java";

let ret = new URL(str);

console.log(ret);

console.log(url.format(ret));
// get
const http = require('http');
const path = require('path');
//const url = require('url');

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

    let obj = new URLSearchParams(req.url.slice(2)); 
    console.log(obj.get('username'));
    res.end(obj.get('password'));
}).listen(3000,()=>{
    console.log('success');
});
// post
const http = require('http');
const fs = require('fs');
const path = require('path');

const host = 'http://localhost:3000';
http.createServer( (req, res) => {
  const { url:input } = req;  // ! 解构赋值
  let url = new URL(input,host);
  console.log(url.pathname,req.method);
  if (url.pathname === '/login') {
    res.writeHead(200,{'Content-Type':'text/html; charset=utf8'});
    res.end(fs.readFileSync(path.join(__dirname,'/post.html')));
  } else if (url.pathname === '/doLogin') {
    let postData = '';
    req.on('data', (chunk) => {
      postData += chunk;
    });
    req.on('end', () => {
      console.log(postData);

      let params = new URLSearchParams(postData);
      console.log(params);

      res.end(postData+'back');
    });
  }

}).listen(3000);

console.log(`Server running at ${host}`);

动态网站开发

/*
    动态网站开发

    成绩查询功能
*/
const fs = require('fs');
const path = require('path');
const http = require('http');
const score = require('./view/score.json');
http.createServer((req,res)=>{

    // * 路由 (请求路径+请求方式)
    // ! 查询成绩的入口地址 /query
    if(req.url.startsWith('/query') && req.method == 'GET'){
        fs.readFile(path.join(__dirname,'view','index.tpl'),'utf8',
        (err,data)=>{
            if(err){
                res.writeHead(500,{'Content-Type':'text/plain;charset=utf8'});
                res.end(err+'Server Error');
            }
            res.end(data);
        });
    }
    else if(req.url.startsWith('/score') && req.method == 'POST'){
        let pdata = '';
        req.on('data',(chunk)=>{
            pdata+=chunk;
        });
        req.on('end',()=>{
            let obj = new URLSearchParams(pdata);
            let result = score[obj.get('code')];

            if(result==undefined){
                res.writeHead(500,{'Content-Type':'text/plain;charset=utf8'});
                res.end('There are no students with this number');
                return;
            }

            fs.readFile(path.join(__dirname,'view','result.tpl'),'utf8',(err,html)=>{
                if(err){
                    res.writeHead(500,{'Content-Type':'text/plain;charset=utf8'});
                    res.end('Server Error');
                }
                html = html.replace('$$chinese$$',result.chinese);
                html = html.replace('$$english$$',result.english);
                html = html.replace('$$math$$',result.math);
                html = html.replace('$$summary$$',result.summary);
                res.end(html);
            });
        });
    }

    // ! 获取成绩的结果 /score

}).listen(3000,()=>{
    console.log('running...');
});

模板引擎 art-template

/*
    模板引擎
*/

let template = require('art-template');
// let html = template(__dirname + '/mytpl.art', {
//     user: {
//         name: 'aui'
//     }
// });

// console.log(html);

/* -------------------------------------------------------------------------- */

// let tpl = '<ul>{{each list value index}}<li>{{value}}</li>{{/each}}</ul>';

// let render = template.compile(tpl);

// let ret = render({
//     list : ['apple','orange','banana']
// });

// console.log(ret);

/* -------------------------------------------------------------------------- */

// let tpl = '<ul>{{each list value index}}<li>{{value}}</li>{{/each}}</ul>';
// let tpl = '<ul>{{each list}}<li>{{$index}}---{{$value}}</li>{{/each}}</ul>';
// let ret = template.render(tpl,{list : ['apple','orange','banana','pineapple']});
// console.log(ret);

/* -------------------------------------------------------------------------- */

let html = template(__dirname + '/result.art', {
    chinese:'123',
    english:'111',
    math:'120',
    summary:'354'

});

console.log(html);

模板引擎实操


const fs = require('fs');
const path = require('path');
const http = require('http');
const score = require('./view/score.json');
const template = require('art-template');
http.createServer((req,res)=>{
    // * 路由 (请求路径+请求方式)
    // ! 查询成绩的入口地址 /query
    if(req.url.startsWith('/query') && req.method == 'GET'){
        let html = template(path.join(__dirname,'view','index.art'),{});
        res.writeHead(200,{'Content-Type':'text/html;charset=utf8'});
        res.end(html);
    }
    else if(req.url.startsWith('/score') && req.method == 'POST'){
        let pdata = '';
        req.on('data',(chunk)=>{
            pdata+=chunk;
        });
        req.on('end',()=>{
            let obj = new URLSearchParams(pdata);
            let result = score[obj.get('code')];

            if(result==undefined){
                res.writeHead(500,{'Content-Type':'text/plain;charset=utf8'});
                res.end('There are no students with this number');
                return;
            }

            let html = template(path.join(__dirname,'view','result.art'),result);
            res.writeHead(200,{'Content-Type':'text/html;charset=utf8'});
            res.end(html);
        });
    }
    else if(req.url.startsWith('/all') && req.method == 'GET'){
        // ! 全部成绩 list 需要是数组
        let list = [];
        for(let key in score){
            list.push(score[key]);
        }
        let html = template(path.join(__dirname,'view','list.art'),{
            list:list
        });
        console.log(html);
        res.writeHead(200,{'Content-Type':'text/html;charset=utf8'});
        res.end(html);   
    }
    // ! 获取成绩的结果 /score

}).listen(3000,()=>{
    console.log('running...');
});
//list.art
<!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=">
    <title>所有信息</title>
</head>
<body>
    <div>
        {{each list}}
        <ul>
            <li>语文:{{$value.chinese}}</li>
            <li>数学:{{$value.math}}</li>
            <li>外语:{{$value.english}}</li>
            <li>综合:{{$value.summary}}</li>
        </ul>
        {{/each}}
    <div>
</body>
</html>

NodeJS原生Web开发总结

/*
    Node.js的web开发相关内容
    - Node.JS不需要依赖第三方应用软件(Apache),可以基于api自己实现
    - 实现静态资源服务器
    - 路由处理
    - 动态网站
    - 模板引擎
    - get 和 post 参数传参和处理

    Web开发框架:express
*/

Express

初步逻辑

const app = require('express')();

// app.get('/' , (req , res)=>{

//    res.send('hello from simple server :)')

// }).listen(3000,()=>{
//     console.log('running...');
// })

let server = app.get('/' , (req , res)=>{

   res.send('hello from simple server :)')

});

server.listen(3000,()=>{
    console.log('running...');
});

use 、static 实现静态资源服务

/*
    托管静态文件

    可以指定虚拟目录

    可以指定多个目录作为静态资源的目录
*/
const express = require('express');
const app = express();
const path = require('path');

// 实现静态资源服务
// use方法的第一个参数可以指定一个虚拟路径
// let server = app.use('/abc',express.static(path.join(__dirname,'public')));
// app.use(express.static(path.join(__dirname,'hello')));
// server.listen(3000,()=>{
//     console.log('running...');
// });
/* -------------------------------------------------------------------------- */
app.use('/abc',express.static(path.join(__dirname,'public')));
app.use(express.static(path.join(__dirname,'hello')));
app.listen(3000,()=>{
     console.log('running...');
 });

路由

/*
    ! 路由(根据请求路径和请求方式进行路径分发)
    ! http常用请求方式:
    - post  添加
    - get   查询
    - put   更新   
    - delete删除

    restful api
*/

const app = require('express')();

// ! 基本的路由处理
// app.get('/' , (req , res)=>{
//    res.send('get data');
// });

// app.post('/' , (req , res)=>{
//    res.send('post data');
// });

// app.put('/' , (req , res)=>{
//    res.send('put data');
// });

// app.delete('/' , (req , res)=>{
//    res.send('delete data');
// });
// ! 直接使用use方法可以处理所有的路由请求
app.use((req,res)=>{
    res.send('ok');
});

app.listen(3000,()=>{
    console.log('running...');
});

应用层中间件

/*
    ! 中间件:处理过程中的一个环节(本质上就是一个函数)
*/

const app = require('express')();
let total = 0;

app.use((req,res,next)=>{
    console.log('有人访问');
    next();
});

app.use('/user',(req,res,next)=>{
    // ! 记录当前访问的时间
    console.log(Date.now());
    // * next方法的作用就是把请求传递到下一个中间件(函数)
    next();
});

app.use('/user',(req,res,next)=>{
    // ! 记录访问日志
    console.log('访问了user');
    next();
});


app.use('/user',(req,res,next)=>{
    total++;
    console.log(total);
    next();
});


app.use('/user',(req,res)=>{
    res.send('result');
});

app.listen(3000,()=>{
    console.log('running...');
});

路由层中间件

/*
    中间件的挂载方式
    use
    路由方式: get post put delete
*/
const express = require('express');
const app = express();

// app.get('abc', (req, res, next) => {
//     console.log(1);
//     // ! 跳转到下一个路由
//     next('route');

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

// app.get('/abc' , (req , res)=>{

//     console.log(3);
//     res.send('hello');
// })

// app.listen(3000, () => {
//     console.log('running...');
// });

/* -------------------------------------------------------------------------- */

var cb0 = function (req, res, next) {
    console.log('CB0');
    next();
  }

  var cb1 = function (req, res, next) {
    console.log('CB1');
    next();
  }

  var cb2 = function (req, res) {
    res.send('Hello from C!');
  }

  app.get('/example', [cb0, cb1, cb2]);

  app.listen(3000, () => {
    console.log('running...');
});

中间件参数处理

/*
    参数处理
*/
const express = require('express');
const app = express();
const path = require('path');
// ! 已被弃用,不建议使用
//const bodyParser = require('body-parser'); 

// 挂载内置中间件
app.use(express.static(path.join(__dirname,'public')));

// 挂载参数处理中间件(post)
app.use(express.urlencoded({ extended: false }));
// 处理json格式的参数
app.use(express.json());

app.get('/login' , (req , res)=>{
    let data = req.query;
    console.log(data);
    res.send(`get ${JSON.stringify(data)}`);
})

app.post('/login' , (req , res)=>{
   let data = req.body;
   console.log(data);
   if(data.username=='admin'  && data.password == '123'){
       res.send('success');
   }
   else
   res.send('failure');
});

app.listen(3000,()=>{
    console.log('running...');
});

使用模板引擎

/*
    模板引擎整合:art-template
*/
const express = require('express');
const path = require('path');
const template = require('art-template');
const app = express();

// 使express兼容art-template模板引擎
app.engine('art', require('express-art-template'));

// 设置模板路径
app.set('views',path.join(__dirname,'views'));
// 设置模板引擎
app.set('view engine','art');

app.get('/list' , (req , res)=>{
    let data = {
        title: '水果',
        list: ['apple','orange','banana']
    }
    // ! 参数一:模板名称;参数二:渲染模板的数据
    res.render('list',data);
});

app.listen(3000,()=>{
    console.log('runnging...');
});

😜 Node.js 前后端交互关键技术

📙 简单图书管理系统


🍀 开发环境

Pasted image 20211112235052.png Pasted image 20211112234756.png Pasted image 20211112235312.png Pasted image 20211112234530.png Pasted image 20211112235349.png


🌏 展示效果

网页效果

Pasted image 20211112234231.png Pasted image 20211112234241.png


数据库

Pasted image 20211112234250.png


文件目录

Pasted image 20211112234316.png


🖥️ 模板引擎搭建系统雏形(后端渲染)


模板文件

<!-- index.art -->
<!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>图书管理系统</title>
    <link rel="stylesheet" href="/www/style.css">  <!-- 静态资源服务 -->
</head>
<body>
    <div class="title">图书管理系统<a href="/toAddBook">添加图书</a></div>
    <div class="content">
        <table cellpadding="0" cellspacing="0"> <!-- 边框紧凑 -->
            <thead>
                <tr>
                    <th>编号</th>
                    <th>名称</th>
                    <th>作者</th>
                    <th>分类</th>
                    <th>描述</th>
                    <th>操作</th>
                </tr>
            </thead> 
            <tbody>
            {{each list}}
                <tr>
                    <td>{{$value.id}}</td>
                    <td>{{$value.name}}</td>
                    <td>{{$value.author}}</td>
                    <td>{{$value.category}}</td>
                    <td>{{$value.description}}</td>
                    <td><a href="/toEditBook?id={{$value.id}}">修改</a>|<a href="/toDeleteBook?id={{$value.id}}">删除</a></td>
                </tr>
            {{/each}}
            </tbody>
        </table>
    </div>
</body>
</html>
<!-- addBook.art -->
<!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>添加图书</title>
</head>
<body>
    <div>添加图书</div>
    <form action="/addBook" method="post">
        名称:<input type="text" name="name"><br>
        作者:<input type="text" name="author"><br>
        分类:<input type="text" name="category"><br>
        描述:<input type="text" name="description"><br>
        <input type="submit" value="提交">
    </form>
</body>
</html>
<!-- editBook.art -->
<!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>添加图书</title>
</head>
<body>
    <div>修改图书</div>
    <form action="/editBook" method="post">
        <input type="hidden" name="id" value="{{id}}"> <!-- 很重要 --> 
        名称:<input type="text" name="name" value="{{name}}"><br>
        作者:<input type="text" name="author" value="{{author}}"><br>
        分类:<input type="text" name="category" value="{{category}}"><br>
        描述:<input type="text" name="description" value="{{description}}"><br>
        <input type="submit" value="提交">
    </form>
</body>
</html>

业务模块

/* 
    业务模块
*/
let data = require('./data.json');
const path = require('path');
const fs = require('fs');

// @ 自动生成图书编号
let maxBookCode = () => {
    let arr = [];
    data.forEach((item) => {
        arr.push(item.id);
    });
    return Math.max.apply(null, arr); // !
};

// @ 渲染主页面
exports.showIndex = (req, res) => {
    res.render('index', {
        list: data
    }); // * 给数组起了个名字叫data
};

// @ 跳转到添加图书的页面
exports.toAddBook = (req, res) => {
    res.render('addBook', {});
};

// @ 添加图书保存数据
exports.addBook = (req, res) => {
    // ! 获取表单数据
    let info = req.body;
    let book = {};
    for (let key in info) {
        book[key] = info[key];
    }
    book.id = maxBookCode() + 1;
    data.push(book);
    // ! 需要把内存中的数据写入到文件
    fs.writeFile(path.join(__dirname, 'data.json'), JSON.stringify(data), (err) => {
        if (err) {
            res.send('server error');
        }
        // ! 文件写入成功之后重新跳转到主页面
        res.redirect('/');
    });
}

// @ 跳转到编辑图书页面
exports.toEditBook = (req, res) => {
    let id = req.query.id; // tip
    let book = {};
    data.forEach((item) => {
        if (item.id == id) {
            book = item;
            return; //break;
        }
    });
    res.render('editBook', book);
};

// @ 编辑图书更新数据
exports.editBook = (req, res) => {
    let info = req.body;
    data.forEach((item) => {
        if (info.id == item.id) {
            for (let key in info) {
                item[key] = info[key];
            }
        }
    });

    // ! 需要把内存中的数据写入到文件
    fs.writeFile(path.join(__dirname, 'data.json'), JSON.stringify(data), (err) => {
        if (err) {
            res.send('server error');
        }
        // ! 文件写入成功之后重新跳转到主页面
        res.redirect('/');
    });
}

路由模块

/*
    路由模块
*/

const express = require('express');
const router = express.Router();
const service = require('./service.js');

// 路由处理

// @ 渲染主页
router.get('/',service.showIndex);

// @ 添加图书(跳转到添加图书的页面)
router.get('/toAddBook',service.toAddBook);

// @ 添加图书(提交表单)
router.post('/addBook',service.addBook);

// @ 跳转到编辑图书信息页面
router.get('/toEditBook',service.toEditBook);

// @ 编辑图书提交表单
router.post('/editBook',service.editBook);


module.exports = router; // @ 导出

入口文件

/*
    图书管理系统 - 入口文件
*/
const express = require('express');
const template = require('art-template');
const path = require('path');
const router = require('./router.js')
const app = express();

// ! 设置模板引擎
// 使express兼容art-template模板引擎
app.engine('art', require('express-art-template'));
// 设置模板路径
app.set('views',path.join(__dirname,'views'));
// 设置模板引擎
app.set('view engine','art');

// ! 处理请求参数
// 挂载参数处理中间件(post)
app.use(express.urlencoded({ extended: false }));
// 处理json格式的参数
app.use(express.json());

// ! 启动静态资源服务
app.use('/www',express.static(path.join(__dirname,'public')));  // @ 第一个参数虚拟路径

// ! 启动服务器功能
// 配置路由
app.use(router);
// 监听端口
app.listen(3000,()=>{
    console.log('running...');
});

json 假数据

[{"id":"1","name":"三国演义","author":"罗贯中","category":"文学","desc":"一个杀伐纷争的年代"},{"id":"2","name":"水浒传","author":"施耐庵","category":"文学","desc":"108条好汉的故事"},{"id":"3","name":"西游记","author":"吴承恩","category":"文学","desc":"佛教与道教的斗争"},{"id":"4","name":"红楼梦","author":"曹雪芹","category":"文学","desc":"一个封建王朝的缩影"},{"name":"陈思艳123","author":"其味无穷二213123","category":"请问请问","desc":"主线程","id":"5","_locals":{}}]

Pasted image 20211112012706.png Pasted image 20211112012721.png


🖥️ Mysql 数据库模块


生成SQL语句

/*
    @ 把data.json文件中的数据拼接成insert语句
*/
const path = require('path');
const fs = require('fs');

fs.readFile(path.join(__dirname,'../','data.json'),'utf8',(err,data)=>{
    if(err) return;
    let list = JSON.parse(data);
    let arr = [];
    list.forEach(item => {
        let sql = `\ninsert into book(name,author,category,description) 
        values('${item.name}','${item.author}','${item.category}','${item.desc}');`;
        arr.push(sql);
    });
    fs.writeFile(path.join(__dirname,'data.sql'),arr.join('').replace(/^\s+/gm, ""),'utf8',(err)=>{
        if(err)console.log("write error");
    });
});

增删改查

连接数据库

/* 
    操作数据库基本步骤
*/
// ! 加载数据库驱动
const mysql = require('mysql');
// ! 创建数据库连接
var connection = mysql.createConnection({
    host: 'localhost',
    user: 'root',
    password: 'sql2008',
    database: 'book'
});

connection.connect();

connection.query('SELECT count(*) as total from book', function (error, results, fields) {
    if (error) throw error;
    console.log('The solution is: ', results[0].total);
});

connection.end();

插入数据

/* 
    插入数据
*/
// ! 加载数据库驱动
const mysql = require('mysql');
// ! 创建数据库连接
var connection = mysql.createConnection({
    host: 'localhost',
    user: 'root',
    password: 'sql2008',
    database: 'book'
});

connection.connect();

let sql = 'insert into book set ?';
let data = {
    name : '明朝那些事',
    author : '当年明月',
    category : '文学',
    description : '明朝的历史'
};

connection.query(sql,data, function (error, results, fields) {
    if (error) throw error;
    // console.log(results);
    if(results.affectedRows==1)console.log('数据插入成功');
});

connection.end();

Pasted image 20211112014257.png


更新数据

/* 
     更新数据
*/
// ! 加载数据库驱动
const mysql = require('mysql');
// ! 创建数据库连接
var connection = mysql.createConnection({
    host: 'localhost',
    user: 'root',
    password: 'sql2008',
    database: 'book'
});

connection.connect();

let sql = 'update book set name=?,author=?,category=?,description=? where id=?';
let data = ['浪潮之巅','吴军','计算机','IT巨头的兴衰史',8];

connection.query(sql,data, function (error, results, fields) {
    if (error) throw error;
    // console.log(results);
    if(results.affectedRows==1)console.log('数据更新成功');
});

connection.end();

Pasted image 20211112014936.png

删除数据

/* 
    删除数据
*/
// ! 加载数据库驱动
const mysql = require('mysql');
// ! 创建数据库连接
var connection = mysql.createConnection({
    host: 'localhost',
    user: 'root',
    password: 'sql2008',
    database: 'book'
});

connection.connect();

let sql = 'delete from book where id = ?';
let data = [7]; // @ 格式上的要求

connection.query(sql,data, function (error, results, fields) {
    if (error) throw error;
    // console.log(results);
    if(results.affectedRows==1)console.log('数据删除成功');
});

connection.end();

查询数据

/* 
    查询数据
*/
// ! 加载数据库驱动
const mysql = require('mysql');
// ! 创建数据库连接
var connection = mysql.createConnection({
    host: 'localhost',
    user: 'root',
    password: 'sql2008',
    database: 'book'
});

connection.connect();

let sql = 'select * from book where id = ?';
let data = [6]; 

connection.query(sql,data, function (error, results, fields) {
    if (error) throw error;
    //console.log(results[1]);
    console.log(results[0].name);
});

connection.end();

封装API

封装脚本

/*
    @ 封装数据库通用API
*/
const mysql = require('mysql');

exports.base = (sql,data,callback)=>{
// ! 加载数据库驱动
const connection = mysql.createConnection({
    host: 'localhost',
    user: 'root',
    password: 'sql2008',
    database: 'book'
});

connection.connect();

// info 数据库操作也是异步的
connection.query(sql,data, function (error, results, fields) {
    if (error) throw error;
    callback(results);
});

connection.end();
}

测试脚本

/*
    测试通用api
*/
const db = require('./db.js');

let sql,data;

// test 插入操作
sql = 'insert into book set ?';
data = {
    name : '笑傲江湖',
    author : '金庸',
    category: '文学',
    description : '武侠小说'
};

db.base(sql,data,res=>{
    console.log(res);
});

// test 更新操作
sql = 'update book set name=?,author=?,category=?,description=? where id = ?';
data = ['天龙八部','金庸','文学','武侠小说',1];

db.base(sql,data,res=>{
    console.log(res);
});

// test 删除操作
sql = 'delete from book where id = ?';
data = [12];
db.base(sql,data,res=>{
    console.log(res);
});

// test 查询操作
sql = 'select * from book where id = ?';
data = [2];
db.base(sql,data,res=>{
    console.log(res);
});

登录验证

/*
    登录验证(前端+后端+数据库)
*/

const express = require('express');
const app = express();
const path = require('path');
const db = require('./db');

app.use(express.urlencoded({ extended: false }));

app.use(express.static(path.join(__dirname,'public')));

app.post('/check' , (req , res)=>{
    let {username,password} = req.body;

    let sql = 'select count(*) as total from users where username = ? and password = ?';
    let data = [username,password];
    db.base(sql,data,result=>{
        if(result[0].total==1){
            res.send('success');
        }
        else{
            res.send('failure');
        }
    });
});

app.listen(3000,()=>{
    console.log('running...');
});


业务模块与Mysql结合

/* 
    业务模块
*/
let data = require('./data.json');
const path = require('path');
const fs = require('fs');
const db = require('./db');

// @ 自动生成图书编号
let maxBookCode = () => {
    let arr = [];
    data.forEach((item) => {
        arr.push(item.id);
    });
    return Math.max.apply(null, arr); // !
};

// @ 渲染主页面
exports.showIndex = (req, res) => {
    let sql = 'select * from book';
    db.base(sql, null, data => {
        res.render('index', {
            list: data
        });
    });
};

// @ 跳转到添加图书的页面
exports.toAddBook = (req, res) => {
    res.render('addBook', {});
};

// @ 添加图书保存数据
exports.addBook = (req, res) => {
    // ! 获取表单数据
    let info = req.body;
    // let book = {};
    // for (let key in info) {
    //     book[key] = info[key];
    // }
    // book.id = maxBookCode() + 1;

    let sql = 'insert into book set ?';
    db.base(sql,info,result=>{
        if(result.affectedRows == 1 ){
            res.redirect('/');
        }
    });
    return;
    data.push(book);
    // ! 需要把内存中的数据写入到文件
    fs.writeFile(path.join(__dirname, 'data.json'), JSON.stringify(data), (err) => {
        if (err) {
            res.send('server error');
        }
        // ! 文件写入成功之后重新跳转到主页面
        res.redirect('/');
    });
}

// @ 跳转到编辑图书页面
exports.toEditBook = (req, res) => {
    let id = req.query.id; // tip
    // let book = {};
    // data.forEach((item) => {
    //     if (item.id == id) {
    //         book = item;
    //         return; //break;
    //     }
    // });
    let sql = 'select * from book where id=?';
    let data = [id];
    db.base(sql,data,result=>{
        if(result.length>0)   
            res.render('editBook', result[0]);
        else
            res.send('查找不到');
    });
};

// @ 编辑图书更新数据
exports.editBook = (req, res) => {
    let info = req.body;

    let sql = 'update book set name=?,author=?,category=?,description=? where id=?';
    let data = [info.name,info.author,info.category,info.description,info.id];
    db.base(sql,data,result=>{
        if(result.affectedRows==1){
            res.redirect('/');
        }
    });

    // data.forEach((item) => {
    //     if (info.id == item.id) {
    //         for (let key in info) {
    //             item[key] = info[key];
    //         }
    //     }
    // });
    // // ! 需要把内存中的数据写入到文件
    // fs.writeFile(path.join(__dirname, 'data.json'), JSON.stringify(data), (err) => {
    //     if (err) {
    //         res.send('server error');
    //     }
    //     // ! 文件写入成功之后重新跳转到主页面
    //     res.redirect('/');
    // });
}

// @ 删除图书信息
exports.toDeleteBook = (req,res) =>{
    let id = req.query.id;
    let sql = 'delete from book where id=?';
    let data = [id];
    db.base(sql,data,result=>{
        console.log(result);
        if(result.affectedRows==1){ // @ affectedRows
            res.redirect('/');
        }
    });
}

🖥️ API 接口开发

后台接口开发

/*
  !  后台接口开发
*/
const express = require('express');
const app = express();
const db = require('./db');

// @ 指定api路径 allBooks (JSON)
app.get('/allBooks',(req,res)=>{
    let sql = 'select * from book';
    db.base(sql,null,result=>{
        res.json(result);
    });
});

// @ 指定api路径 allBooks (JSONP)
app.set('jsonp callback name','call');

app.get('/allBooks2',(req,res)=>{
    let sql = 'select * from book';
    db.base(sql,null,result=>{
        res.jsonp(result);
    });
});

app.listen(3000,()=>{
    console.log('running...');
});

restful api

/*
    ! restful api 是从URL的格式来表述的
    get    http://localhost:3000/books
    get    http://localhost:3000/books/book
    post   http://localhost:3000/books/book
    get    http://localhost:3000/books/book/1
    put    http://localhost:3000/books/book    // @ 更新数据
    delete http://localhost:3000/books/book/1

    @ 传统URL风格
    http://localhost:3000/
    http://localhost:3000/toAddBook
    http://localhost:3000/addBook
    http://localhost:3000/toEditBook?id=1
    http://localhost:3000/editBook
    http://localhost:3000/toDeleteBook?id=2
*/
const express = require('express');
const app = express();
const db = require('./db');

app.get('/books' , (req , res)=>{
    let sql = 'select * from book';
    db.base(sql,null,result=>{
        res.json(result);
    });  
});

// http://localhost:3000/books/book/1
app.get('/books/book/:id' , (req , res)=>{
    let id = req.params.id;
    let sql = 'select * from book where id = ?';
    let data = [id];
    db.base(sql,data,result=>{
        res.json(result[0]);
    });  
})

app.listen(3000,()=>{
    console.log('running...');
});

⭐ 前端渲染开发(前端渲染)


Restful API


路由模块

const db = require('./db');
const {base:query} = db;

exports.allBooks = (req,res)=>{
    let sql = 'select * from book';
    query(sql,null,result=>{
        res.json(result);
    });
};

exports.addBook = (req,res)=>{
    let info = req.body;
    // test
    delete info.id;
    let sql = 'insert into book set ?';  // @ 要一个对象!
    query(sql,info,(result)=>{
        if(result.affectedRows==1){
            res.json({flag:1});
        }
        else{
            res.json({flag:2});
        }
    })
};

exports.getBookById = (req,res)=>{
    let id = req.params.id;
    let sql = 'select * from book where id=?';
    let data = [id];
    query(sql,data,result=>{
        res.json(result[0]);
    });
};

exports.editBook = (req,res)=>{
    let info = req.body;
    let sql = 'update book set name=?,author=?,category=?,description=? where id = ?';
    let data = [info.name,info.author,info.category,info.description,info.id];
    query(sql,data,(result)=>{
        if(result.affectedRows==1){
            res.json({flag:1});
        }
        else{
            res.json({flag:2});
        }
    });
};

exports.deleteBook = (req,res)=>{
    let id = req.params.id;
    let sql = 'delete from book where id = ?';
    let data = [id];
    query(sql,data,(result)=>{
        if(result.affectedRows==1){
            res.json({flag:1});
        }
        else{
            res.json({flag:2});
        }
    });
};

业务处理模块

const db = require('./db');
const {base:query} = db;


exports.allBooks = (req,res)=>{
    let sql = 'select * from book';
    query(sql,null,result=>{
        res.json(result);
    });
};

exports.addBook = (req,res)=>{
    let info = req.body;
    // test
    delete info.id;
    let sql = 'insert into book set ?';  // @ 要一个对象!
    query(sql,info,(result)=>{
        if(result.affectedRows==1){
            res.json({flag:1});
        }
        else{
            res.json({flag:2});
        }
    })
};

exports.getBookById = (req,res)=>{
    let id = req.params.id;
    let sql = 'select * from book where id=?';
    let data = [id];
    query(sql,data,result=>{
        res.json(result[0]);
    });
};

exports.editBook = (req,res)=>{
    let info = req.body;
    let sql = 'update book set name=?,author=?,category=?,description=? where id = ?';
    let data = [info.name,info.author,info.category,info.description,info.id];
    query(sql,data,(result)=>{
        if(result.affectedRows==1){
            res.json({flag:1});
        }
        else{
            res.json({flag:2});
        }
    });
};

exports.deleteBook = (req,res)=>{
    let id = req.params.id;
    let sql = 'delete from book where id = ?';
    let data = [id];
    query(sql,data,(result)=>{
        if(result.affectedRows==1){
            res.json({flag:1});
        }
        else{
            res.json({flag:2});
        }
    });
};

Mysql API

/*
    @ 封装数据库通用API
*/
const mysql = require('mysql');

exports.base = (sql,data,callback)=>{
// ! 加载数据库驱动
const connection = mysql.createConnection({
    host: 'localhost',
    user: 'root',
    password: 'sql2008',
    database: 'book'
});

connection.connect();

// info 数据库操作也是异步的
connection.query(sql,data, function (error, results, fields) {
    if (error) {
        console.log(error);
        return;
    }
    callback(results);
});

connection.end();
}

入口文件

/*
  ! 实现图书管理系统所有的后台接口
*/

const express = require('express');
const router = require('./router.js');
const app = express();
const path = require('path');

app.use('/www',express.static(path.join(__dirname,'public')));
app.use(express.urlencoded({extended:false}));
app.use(router);

app.listen(3000,()=>{
    console.log('running...');
});

💡 前端开发( IMPORTANT )


Common CSS

/* style.css */

.title {
    text-align: center;
    background-color: indianred;
    height: 50px;
    line-height: 50px;
    font-size: 18px;
}

.content {
    background-color: lightblue;
}

.content table {
    width: 100%;
    text-align: center;
    border-right: 1px solid orange;
    border-bottom: 1px solid orange;
}

.content td,
th {
    border-left: 1px solid orange;
    border-top: 1px solid orange;
    height: 40px;
    line-height: 40px;
}

.form{
    display: none;
    position: absolute;
    left: 50%;
    margin-left: -110px;
    top: 100px;
}

Common HTML

<!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>图书管理系统</title>
    <link rel="stylesheet" href="/www/css/style.css">  <!-- 静态资源服务 -->
    <script type="text/javascript" src="/www/js/dialog.js"></script>
    <script type="text/javascript" src="/www/js/jquery.js"></script>
    <script type="text/javascript" src="/www/js/template-web.js"></script>
    <script type="text/javascript" src="/www/js/index_ajax.js"></script>
    <script type="text/template" id="indexTpl">
            {{each list}}
                <tr>
                    <td>{{$value.id}}</td>
                    <td>{{$value.name}}</td>
                    <td>{{$value.author}}</td>
                    <td>{{$value.category}}</td>
                    <td>{{$value.description}}</td>
                    <td><a href="javascript:;">修改</a>|<a href="javascript:;">删除</a></td>
                </tr>
            {{/each}}
    </script>
</head>
<body>
    <div class="title">图书管理系统<a id="addBookId" href="javascript:void(0);">添加图书</a></div>
    <div class="content">
        <table cellpadding="0" cellspacing="0"> <!-- 边框紧凑 -->
            <thead>
                <tr>
                    <th>编号</th>
                    <th>名称</th>
                    <th>作者</th>
                    <th>分类</th>
                    <th>描述</th>
                    <th>操作</th>
                </tr>
            </thead> 
            <tbody id="dataList">


            </tbody>
        </table>
    </div>

    <form class="form" id="addBookForm">
        <!-- @ 需要一个隐藏域 -->
        <input type="hidden" name="id"> 
        名称:<input type="text" name="name"><br>
        作者:<input type="text" name="author"><br>
        分类:<input type="text" name="category"><br>
        描述:<input type="text" name="description"><br>
        <input type="button" value="提交">
    </form>
</body>
</html>

HTML JQUERY AJAX

$(function () {
    // @ 渲染列表数据
    function initList() {
        $.ajax({
            type: 'get',
            url: '/books',
            dataType: 'json',
            success: function (data) {
                // 渲染数据列表
                var html = template('indexTpl', {
                    list: data
                });
                $('#dataList').html(html);
                // @ 必须在渲染完成内容之后才可以操作DOM标签
                $('#dataList').find('tr').each((index, elem) => {
                    var td = $(elem).find('td:eq(5)');

                    var id = $(elem).find('td:eq(0)').text();
                    // @ 绑定编辑图书的单击事件
                    td.find('a:eq(0)').click(() => {
                        editBook(id);
                    });
                    // @ 绑定删除图书的单击事件
                    td.find('a:eq(1)').click(() => {
                        deleteBook(id);
                    });

                    // @ 绑定添加图书信息的单击事件
                    addBook(); // test ?

                    // @ 操作完成之后重置表单
                    var form = $('#addBookForm');
                    form.get(0).reset(); // ! 重置表单(但是不会对隐藏域进行处理)
                    form.find('input[type=hidden]').val('');
                });
            }
        });
    };
    initList();

    // @ 编辑图书信息
    function editBook(id) {
        var form = $('#addBookForm');
        // 先根据数据id查询最新的数据
        $.ajax({
            type: 'get',
            url: '/books/book/' + id,
            dataType: 'json',
            success: data => {
                // 生成弹出窗口
                var mark = new MarkBox(600, 400, '编辑图书', form.get(0));
                mark.init();
                // 填充表单数据
                form.find('input[name=id]').val(data.id);
                form.find('input[name=name]').val(data.name);
                form.find('input[name=author]').val(data.author);
                form.find('input[name=category]').val(data.category);
                form.find('input[name=description]').val(data.description);
                // 对表单提交按钮重新绑定单机事件
                form.find('input[type=button]').unbind('click').click(() => {
                    // 编辑完成数据之后重新提交表单
                    $.ajax({
                        type : 'put',
                        url : '/books/book',
                        data : form.serialize(),
                        dataType : 'json',
                        success : data=>{
                            if(data.flag=='1'){
                                // 隐藏弹窗
                                mark.close();
                                // 重新渲染数据列表
                                initList();
                            }
                        }
                    });
                });
            }
        });
    }

    // fixme 表格序列化过程中,会带入id值的问题
    // @ 添加图书信息
    function addBook() {
        $('#addBookId').click(() => {
            var form = $('#addBookForm');
            // 实例化弹窗对象
            var mark = new MarkBox(600, 400, '添加图书', form.get(0));
            mark.init();

            form.find('input[type=button]').unbind('click').click(() => {
                $.ajax({
                    type: 'post',
                    url: '/books/book',
                    data: form.serialize(),
                    dataType: 'json',
                    success: data => {
                        if (data.flag == '1') {
                            // @ 关闭弹窗
                            mark.close();
                            // @ 添加图书之后重新渲染数据列表
                            initList();
                        }
                    }
                });
            });
        });
    }

    // @ 删除图书信息
    function deleteBook(id){
        $.ajax({
            type : 'delete',
            url : '/books/book/'+id,
            dataType : 'json',
            success : data=>{
                // 删除图书信息之后重新渲染数据列表
                if(data.flag == '1'){
                    initList();
                }
            }
        });
    }
});

HTML JQUERY AXIOS

Pasted image 20211112231103.png Pasted image 20211112231917.png

// @ 使用 Axios 进行网络请求接收处理
$(function () {
    // @ 渲染列表数据
    function initList() {
        axios.get('/books')
            .then(res => {
                // @ 处理获取的数据
                // ^ 将数据填充到表格
                var html = template('indexTpl', {
                    list: res.data
                });
                $('#dataList').html(html);
                // ^ 为每行tr的a标签写入事件
                $('#dataList').find('tr').each((index, elem) => {
                    var td = $(elem).find('td:eq(5)');

                    var id = $(elem).find('td:eq(0)').text();
                    // @ 绑定编辑图书的单击事件
                    td.find('a:eq(0)').click(() => {
                        editBook(id);
                    });
                    // @ 绑定删除图书的单击事件
                    td.find('a:eq(1)').click(() => {
                        deleteBook(id);
                    });

                    // @ 绑定添加图书信息的单击事件
                    addBook(); // test ?

                    // @ 操作完成之后重置表单
                    var form = $('#addBookForm');
                    form.get(0).reset(); // ! 重置表单(但是不会对隐藏域进行处理)
                    form.find('input[type=hidden]').val('');
                });

            })
            .catch(err => {
                console.error(err);
            });
    }

    initList();

    // @ 编辑图书信息
    function editBook(id) {
        var form = $('#addBookForm');
        // 先根据数据id查询最新的数据
        axios.get('/books/book/' + id)
            .then(res => {
                let data = res.data;
                // 生成弹出窗口
                var mark = new MarkBox(600, 400, '编辑图书', form.get(0));
                mark.init();
                // 填充表单数据
                form.find('input[name=id]').val(data.id);
                form.find('input[name=name]').val(data.name);
                form.find('input[name=author]').val(data.author);
                form.find('input[name=category]').val(data.category);
                form.find('input[name=description]').val(data.description);
                // 对表单提交按钮重新绑定单机事件
                form.find('input[type=button]').unbind('click').click(() => {
                    // 编辑完成数据之后重新提交表单
                    axios.put('/books/book', form.serialize())
                        .then(res => {
                            let data = res.data;
                            if (data.flag == '1') {
                                // 隐藏弹窗
                                mark.close();
                                // 重新渲染数据列表
                                initList();
                            }
                        })
                        .catch(err => {
                            console.error(err);
                        })
                })
            })
            .catch(function (error) {
                console.log(error);
            });
    }

    // fixme 表格序列化过程中,会带入id值的问题
    // @ 添加图书信息
    function addBook() {
        $('#addBookId').click(() => {
            var form = $('#addBookForm');
            // 实例化弹窗对象
            var mark = new MarkBox(600, 400, '添加图书', form.get(0));
            mark.init();

            form.find('input[type=button]').unbind('click').click(() => {

                axios.post('/books/book', form.serialize())
                    .then(res => {
                        if (res.data.flag == '1') {
                            // @ 关闭弹窗
                            mark.close();
                            // @ 添加图书之后重新渲染数据列表
                            initList();
                        }
                    })
                    .catch(err => {
                        console.error(err);
                    });
            });
        });
    }

    // @ 删除图书信息
    function deleteBook(id) {
        axios.delete('/books/book/' + id)
            .then(res => {
                // 删除图书信息之后重新渲染数据列表
                if (res.data.flag == '1') {
                    initList();
                }
            })
            .catch(err => {
                console.error(err);
            })
    }
});

🖥️ 服务器主动发送请求

Select

/*
    ! 从服务器主动发送请求调用接口-查询数据
*/

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

const options = {
    hostname : 'localhost',
    port : 3000,
    path : '/books'
};


var req = http.request(options,(res)=>{
    let info = '';
    res.on('data',(chunk)=>{
        info+=chunk;
    });
    res.on('end',()=>{
        console.log(info);
    });
});

req.end();

Insert

/*
    ! 从服务器主动发送请求调用接口-添加数据
*/

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

const options = {
    hostname : 'localhost',
    port : 3000,
    path : '/books/book',
    method : 'post',
    headers :{
        'Content-Type': 'application/x-www-form-urlencoded',
    }
};

let data = new URLSearchParams({
    name : 'adafa',
    author : 'xxxx',
    category : 'xasdasd',
    description : 'hahaha'
}).toString(); // @ 字符串化

var req = http.request(options,(res)=>{
    let info = '';
    res.on('data',(chunk)=>{
        info+=chunk;
    });
    res.on('end',()=>{
        console.log(info);
    });
});

// @ req写入Body
req.write(data); 
req.end();

Select by id

/*
    ! 从服务器主动发送请求调用接口-选择数据
*/

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


const options = {
    hostname : 'localhost',
    port : 3000,
    path : '/books/book/24',
    method : 'get'
};

var req = http.request(options,(res)=>{
    let info = '';
    res.on('data',(chunk)=>{
        info+=chunk;
    });
    res.on('end',()=>{
        console.log('\x1B[1m %s \x1B[1m',info);
    });
});

req.end();

Update

/*
    ! 从服务器主动发送请求调用接口-更新数据
*/

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


const options = {
    hostname : 'localhost',
    port : 3000,
    path : '/books/book',
    method : 'put', 
    headers :{
        'Content-Type': 'application/x-www-form-urlencoded',
    }
};

let data = new URLSearchParams({
    id : '21',
    name : 'adafa',
    author : 'xxxx',
    category : 'xasdasd',
    description : 'hahaha'
}).toString(); // @ 字符串化

var req = http.request(options,(res)=>{
    let info = '';
    res.on('data',(chunk)=>{
        info+=chunk;
    });
    res.on('end',()=>{
        console.log('\x1B[1m %s \x1B[1m',info);
    });
});

req.write(data);
req.end();

Delete

/*
    ! 从服务器主动发送请求调用接口-删除数据
*/

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


const options = {
    hostname : 'localhost',
    port : 3000,
    path : '/books/book/22',
    method : 'delete', 
    headers :{
        'Content-Type': 'application/x-www-form-urlencoded',
    }
};

var req = http.request(options,(res)=>{
    let info = '';
    res.on('data',(chunk)=>{
        info+=chunk;
    });
    res.on('end',()=>{
        console.log('\x1B[1m %s \x1B[1m',info);
    });
});

req.end();

⭐调用第三方接口

直接访问

/*
    ! 从服务器主动发送请求调用第三方接口
*/

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


let citycode = '101010100'
const options = {
    hostname : 'www.weather.com.cn',
    port : 80,
    path : `/data/sk/${citycode}.html`,
    method : 'get' 
};

var req = http.request(options,(res)=>{
    let info = '';
    res.on('data',(chunk)=>{
        info+=chunk;
    });
    res.on('end',()=>{
        console.log('\x1B[1m %s \x1B[1m',info);
    });
});

req.end();


封装访问方法

/*
    ! 从服务器主动发送请求调用第三方接口
*/

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

exports.queryWether = (cityCode,callback)=>{
    //let cityCode = '101010100'
    const options = {
        hostname : 'www.weather.com.cn',
        port : 80,
        path : `/data/sk/${cityCode}.html`,
        method : 'get' 
    };

    var req = http.request(options,(res)=>{
        let info = '';
        res.on('data',(chunk)=>{
            info+=chunk;
        });
        res.on('end',()=>{
            //console.log('\x1B[1m %s \x1B[1m',info);
            callback(JSON.parse(info)); // @ 转换成对象
        });
    });

    req.end();
};
const weather = require('./1-7 third serverapi.js');

weather.queryWether('101020100',data=>{
    let {weatherinfo:info} = data;
    console.log(`${info.city} 今天风向是 ${info.WD}`);
});

😜 Vue + Node.JS + EasyUI ①

express.urlencoded

bodyParser.urlencoded({ })里extended: true和false区别??? Pasted image 20211120151536.png

🌏 制作CRUD表格( 单Vue页面 )

⏰ 开发时长:10 小时

(从零开始|疯狂采坑)

👀 效果展示

Pasted image 20211121040840.png Pasted image 20211121040850.png

Mysql 代码(后续测试)

# 部分存储过程不列出
use mysales;

drop procedure if exists sp1;
delimiter $$
create procedure sp1(
)
begin
select * from products;
end $$
delimiter ;

call sp1();

drop procedure if exists sp2;
delimiter $$
create procedure sp2(
$productid int
)
begin
select * from products where productid = $productid;
end $$
delimiter ;

call sp2(10);

drop procedure if exists sp3;
delimiter $$
create procedure sp3(
$categoryid varchar(1),
$unitprice decimal(8,2)
)
begin
select * from products where categoryid = $categoryid and unitprice>$unitprice;
end $$
delimiter ;

call sp3('A',30);

drop procedure if exists getid;
delimiter $$
create procedure getid()
begin
select max(productid)+1 as productid from products;
end $$
delimiter ;

call getid()

drop procedure if exists ifid;
delimiter $$
create procedure ifid($productid int)
begin
select count(*) as flag from products where productid = $productid;
end $$
delimiter ;

call ifid(141);

后端开发

index.js 入口

const express = require('express')
const app = express();
const router = require('./router');

const port = 3000;

// @ 请求处理函数
app.use(express.urlencoded({extended:false}));
app.use(express.json());

// @ 配置路由
app.use(router);

// @ 监听端口
app.listen(port,()=>{
    console.log('running...');
});

总结

这里跟以前没有多大区别,只要记得请求处理函数和配置路由的使用
还有模板引擎的使用方法

router.js 路由

const express = require('express');
const router = express.Router();
const service = require('./service');

router.get('/' , (req , res)=>{
   res.send('hello from simple server :)')
})

router.get('/products',service.showProducts);

router.post('/crudProducts' , service.crudProducts);

router.post('/myRunSqlProcedure',service.myRunSqlProcedures);

module.exports = router;

总结

路由上依旧使用 Express,将路由与业务逻辑分开来写

service.js 业务

const path = require('path');
const fs = require('fs');
const {base:query,runEditRows, myRunSqlProcedure} = require('./db/db');

exports.showProducts = (req,res)=>{
    let sql = 'select productid,productname,englishname,quantityperunit,categoryid,unit,unitprice from products';
    let data = [];

    query(sql,data,results=>{
        res.json(results);
    }); 
}

exports.crudProducts = (req,res)=>{
    let crud = req.body;
    //console.log(crud);
    runEditRows(crud,(results)=>{
        //console.log(results);
        res.json(results);
    });
}

// @ name,params
exports.myRunSqlProcedures = (req,res)=>{
    let crud = req.body;
    myRunSqlProcedure(crud.name,crud.params,(results)=>{
        //console.log(results);
        res.json(results);
    });  
}

业务模块里用到了另一个封装的数据库模块

这里只加了一个GET接口,两个POST接口

分别是获取所有商品,执行crud,执行存储过程

⭐ db.js 数据库封装

/*
    @ 封装数据库通用API
*/
const mysql = require('mysql');

const myDatabase = 'mysales';

function myConnect(){
    const connection = mysql.createConnection({
        host: 'localhost',
        user: 'root',
        password: 'sql2008',
        database: myDatabase
    });
    connection.connect();
    return connection; // warn 不是返回 connection.connect();
}


exports.base = (sql,data,callback)=>{
    // ! 加载数据库驱动
    const connection = myConnect();
    // info 数据库操作也是异步的
    connection.query(sql,data, function (error, results, fields) {
        if (error) {
          console.log(error);
          return;   
        }//throw error;
        callback(results);
    });
    connection.end();
}


exports.runEditRows = ({database='',tablename=myDatabase,keyfield='',sortfield='',data=[]} = {},callback)=>{
    //console.log('被执行了');
    const connection = myConnect();
    if(tablename==''||keyfield==''||data==[]){
        console.log('runEditRows 失败,参数有误');
        callback({'_error':'runEditRows 失败,参数有误'});
        return;
    }
    if(Array.isArray(data)) data = JSON.stringify(data); // @ 如果data是js对象数组则转化成JSON字符串
    let sql = 'call sys_runEditRows(?,?,?,?,?);'; // @ 生成 sql 语句
    let p = [database,tablename,keyfield,sortfield,data]; // @ 定义数组p用于替换sql语句里的?
    connection.query(sql,p, function (error, results, fields) {
         if (error) {
            console.log(error);
            return;   
          }//throw error;
        if(results[0].length==0)results.shift();
        callback(results);
    });
    connection.end();
}


exports.myRunSqlProcedure = (procedureName='',p={},callback)=>{
    const connection = myConnect();
    let sql;
    let list;
    if(procedureName==''||p=={}){
        console.log('myRunSqlProcedure 失败,传入参数不对');
        callback({'_error':'myRunSqlProcedure 失败,传入参数不对'});
        return;
    }
    sql = `SHOW CREATE PROCEDURE ${myDatabase}.${procedureName}`;
    list = [];
    connection.query(sql,[], function (error, results, fields) {
        if (error) {
            console.log(error);
            return;   
          }
        let columns = results[0]['Create Procedure']; 
        columns = columns.substring(columns.indexOf('(')+1,columns.indexOf('begin')-1);
        columns = columns.split('$');
        if(columns[0]=='\n'||columns[0]=='') columns.shift(); 
        columns //?
        columns.forEach(element => {
            list.push(element.substring(0,element.indexOf(' ')))
        });
        if(list[0]=='') list = []; // test

        // step 获取对象属性个数
        var count = 0;
        for(var i in p) {
            if(p.hasOwnProperty(i)) {  
                count++;
            }
        }
        if(count<list.length){
            console.log(`传入参数个数与存储过程参数个数不匹配(过少),要求的参数个数:${list.length},你的个数:${count}`)
            callback({'_error':`传入参数个数与存储过程参数个数不匹配(过少),要求的参数个数:${list.length},你的个数:${count}}`});
            return;
        }
        // step 遍历 list
        for(var i=0;i<list.length;i++){
            let temp = p[ list[i] ]; // md 转 html 这里会出错
            if(temp==undefined) break;
            if(typeof(temp)=='object')temp = JSON.stringify(temp);
            list[i] = temp;
        }
        if(i!=list.length){
            console.log(`对象里的属性名称与存储过程$变量名称( $${list[i]} )不匹配`)
            callback({'_error':`对象里的属性名称与存储过程$变量名称( $${list[i]} )不匹配`});
            return;
        }

        let sql = `call ${procedureName}(`;
        for(var i=0;i<list.length;i++){
            if(i==list.length-1)
            sql += '?'
            else
            sql += '?,'
        }
        sql+=')';

        // step 执行存储过程
        connection.query(sql,list, function (error, results, fields) {
            if (error) {
               console.log(error);
               return;   
             }
           callback(results);
           connection.end();
       });
       //connection.end();
    });
}
// test
// exports.myRunSqlProcedure('ifid',{productid:141},(results)=>{
//     console.log(results);
// })

⭐ 接口测试

Pasted image 20211121041949.png Pasted image 20211121041938.png Pasted image 20211121042002.png

用来测试接口的JSON数据
http://localhost:3000/myRunSqlProcedure

{
    "name":"sp1",
    "params":{}
}

{
    "name":"sp2",
    "params":{"productid":100}
}

{
    "name":"sp3",
    "params":{"categoryid":"A","aaaaaaaa":30}
}

{
    "name":"sp3",
    "params":{"categoryid":"A"}
}

{
    "name":"sp3",
    "params":{"categoryid":"A","unitprice":30}
}

{
    "name":"ifid",
    "params":{"productid":"122"}
}
http://localhost:3000/myRunSqlProcedure

{
    "tablename":"products",
    "keyfield":"productid",
    "sortfield":"",
    "data":[{
        "_action":"update",
        "productid":10,
        "unitprice":200
    }]
}

前端开发

vue.config.js 跨域问题

const path = require('path');
function resolve (dir) {
    return path.join(__dirname, dir)
}
module.exports = {
    lintOnSave: true,
    runtimeCompiler: true, // 使用构建版vue
    chainWebpack: (config)=>{
        config.resolve.alias
            .set('assets',resolve('src/assets'))
            .set('components',resolve('src/components'))
            //.set('easyui',resolve('src/easyui')) // 我这边是购买 了源代码,直接复制到src目录下使用

    },
    devServer: {
        proxy: {
            '/api': {
                // 此处的写法,目的是为了 将 /api 替换成 http://localhost:3000
                target: 'http://localhost:3000',
                // 允许跨域
                changeOrigin: true,
                ws: true,
                pathRewrite: {
                    '^/api': ''
                }
            }
        }
    }
}

main.js 入口

import Vue from 'vue'
import App from './App.vue'
import 'vx-easyui/dist/themes/default/easyui.css';
import 'vx-easyui/dist/themes/icon.css';
import 'vx-easyui/dist/themes/vue.css';
import EasyUI from 'vx-easyui';
import Axios from 'axios' 

Vue.use(EasyUI);

Vue.config.productionTip = false

// step2:把axios挂载到vue的原型中,在vue中每个组件都可以使用axios发送请求,
// @ 不需要每次都 import一下 axios了,直接使用 $axios 即可
Vue.prototype.$axios = Axios

// @ 解决跨域问题
Axios.defaults.baseURL = '/api'

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

step2:把axios挂载到vue的原型中,在vue中每个组件都可以使用axios发送请求,
不需要每次都 import一下 axios了,直接使用 $ axios 即可

Vue.prototype.$axios = Axios

解决跨域问题

Axios.defaults.baseURL = '/api'

App.vue

<template>
    <div style="margin:4px;position:absolute;top:0;bottom:0;left:0;right:0;">
        <Layout style="" :layoutStyle="{
     position : 'absolute',
     top : 0,
     bottom : 0,
     left : 0,
     right : 0
     }">
            <LayoutPanel region="north" style="height:40px;overflow:hidden;" :bodyStyle="{'overflow':'hidden'}">
                <Panel :bodyStyle="{'padding':'5px','margin-left':'100px'}" :border="false">
                    <LinkButton iconCls="icon-add" @click='add' :plain="true">新增</LinkButton>
                    <LinkButton iconCls="icon-edit" @click="edit" :plain="true">修改</LinkButton>
                    <LinkButton iconCls="icon-remove" @click="del" :plain="true">删除</LinkButton>
                    <LinkButton iconCls="icon-search" @click="filter=!filter" :plain="true" style="display:none;">过滤
                    </LinkButton>
                    <LinkButton iconCls="icon-help" :plain="true">帮助</LinkButton>
                </Panel>
            </LayoutPanel>
            <LayoutPanel region="south" style="height:60px;">
                <div style="position:absolute;left:100px;top:10px;">你选中了 {{selection}}</div>
            </LayoutPanel>
            <LayoutPanel region="west" style="width:100px;">
                <div>左边的区域</div>
            </LayoutPanel>
            <LayoutPanel region="center" style="height:100%;">
                <DataGrid :data="data" style="height:100%;" :border="false" :columnResizing="true" :multiSort="true"
                    :filterable="filter" ref="dg" selectionMode="single" @selectionChange="selection=$event">
                    >
                    <GridColumn field="productid" title="商品编码" width="80" align="center" :frozen="true"
                        :sortable="true"></GridColumn>
                    <GridColumn field="categoryid" title="商品类别" width="80" align="center" :frozen="true"
                        :sortable="true">
                        <template slot="filter">
                            <ComboBox class="f-full" :data="categories" :editable="false"
                                :inputStyle="{textAlign:'center'}"
                                @valueChange="$refs.dg.doFilter({field:'categoryid',op:'equal',value:$event.currentValue})">
                            </ComboBox>
                        </template>
                    </GridColumn>
                    <GridColumn field="productname" title="商品名称" width="200" align="center"></GridColumn>
                    <GridColumn field="englishname" title="英文名称" width="400" align="center"></GridColumn>
                    <GridColumn field="unitprice" title="商品价格" align="right" width="100" halign="center"
                        :sortable="true"></GridColumn>
                    <GridColumn field="quantityperunit" title="单位数量" align="center"></GridColumn>
                    <GridColumn field="unit" title="商品单位" width="80" align="center">
                    </GridColumn>
                </DataGrid>
            </LayoutPanel>
        </Layout>


        <Dialog ref="dlg" bodyCls="f-column" :title="title" :modal="true" iconCls="icon-edit" closed @close="cancel"
            :dialogStyle="{height:'500px',width:'440px'}">
            <div class="f-full" style="overflow:hidden">
                <Form :model="model" :rules="rules" :labelWidth="80" labelAlign="right"
                    style="padding:15px 20px 20px 10px;">
                    <FormField name="productid" label="商品编码:">
                        <NumberBox ref="productid" :disabled="showkeyfield" v-model="model.productid"></NumberBox>
                    </FormField>
                    <FormField name="categoryid" label="商品类别:">
                        <ComboBox :data="dlgcategories" v-model="model.categoryid"></ComboBox>
                    </FormField>
                    <FormField name="productname" label="商品名称:">
                        <TextBox v-model="model.productname"></TextBox>
                    </FormField>
                    <FormField name="englishname" label="英文名称:">
                        <TextBox v-model="model.englishname"></TextBox>
                    </FormField>
                    <FormField name="unitprice" label="商品价格:">
                        <NumberBox :precision="2" v-model="model.unitprice"></NumberBox>
                    </FormField>
                    <FormField name="quantityperunit" label="单位数量:">
                        <TextBox v-model="model.quantityperunit"></TextBox>
                    </FormField>
                    <FormField name="unit" label="商品单位:">
                        <TextBox v-model="model.unit"></TextBox>
                    </FormField>
                    <FormField style="float:right;">
                        <LinkButton iconCls="icon-ok" @click="ok()" style="width:100px">Save</LinkButton>
                        <LinkButton iconCls="icon-no" @click="cancel()" style="width:100px">Cancel</LinkButton>
                    </FormField>
                </Form>
            </div>

        </Dialog>

    </div>
</template>

<script>
    //import axios from 'axios';
    import LoadPrompt from './prompt.js';
    import './prompt.css';
    import {
        myRunSqlProcedure
    } from './myFunctions';

    export default {
        data() {
            return {
                data: [{
                    productid: 'test'
                }],
                categories: [{
                    value: null,
                    text: "All"
                }],
                dlgcategories: [],
                filter: true, // warn meaningless
                selection: null,
                // @ diglog
                title: '测试窗口',
                model: {
                    productid: null,
                    categoryid: null,
                    productname: null,
                    englishname: null,
                    unitprice: null,
                    quantityperunit: null,
                    unit: null
                },
                rules: {
                    productid: 'required',
                    categoryid: 'required',
                    productname: 'required'
                },
                showkeyfield: false,
                // @ crud
                p: {
                    tablename: 'products',
                    keyfield: 'productid',
                    sortfield: '',
                    data: []
                },
                // @ prompt
                prompt: null
            }
        },
        methods: {
            getCategories(data) {
                data.forEach((item) => {
                    if (this.categories.findIndex(value => value.value == item.categoryid) == -1) {
                        var t = {
                            value: item.categoryid,
                            text: item.categoryid
                        }
                        this.categories.push(t);
                        this.dlgcategories.push(t);
                    }
                })
            },
            del() {
                if (this.selection == null) {
                    this.$messager.alert({
                        title: "警告",
                        icon: "info",
                        msg: "请选中一行!"
                    });
                    var a = this.prompt.error('请先选中一行', true);
                    setTimeout(function () {
                        a.remove()
                    }, 2000)
                    return;
                }

                this.$messager.confirm({
                    title: "确认删除",
                    msg:  `你确定要删除 ${this.selection.productid} ${this.selection.productname}?`,
                    icon: 'icon-info',
                    result: r => {
                        if (r) {

                            this.p.data = [];
                            let row = {};
                            row._action = 'delete';
                            row.productid = this.selection.productid;
                            //row._reloadrow = 1;
                            this.p.data.push(row);

                            this.$axios.post('/crudProducts', this.p)
                                .then(res => {
                                    //console.log(res.data[1][0]);
                                    if (res.data[0][0]._error == '') {
                                        let index = this.data.indexOf(this.selection);
                                        this.data.splice(index, 1);
                                        if (index == this.data.length) index--;
                                        this.selection = this.data[index];
                                        this.$refs.dg.selectRow(this.selection);
                                        this.prompt.success('删除成功')
                                    }
                                })
                                .catch(err => {
                                    console.error(err);
                                })

                        }
                    }
                });
            },
            add() {
                myRunSqlProcedure('getid', {}, (results) => {
                    this.model.productid = results[0].productid;
                    this.prompt.inform(`自动填充 Productid ${results[0].productid}`);
                });
                this.model = {
                    productid: null,
                    categoryid: 'A',
                    productname: null,
                    englishname: null,
                    unitprice: 0,
                    quantityperunit: null,
                    unit: null
                };
                this.title = '添加行';
                this.$refs.dlg.open();
            },
            edit() {
                if (this.selection == null) {
                    this.$messager.alert({
                        title: "警告",
                        icon: "info",
                        msg: "请选中一行!"
                    });
                    var a = this.prompt.error('请先选中一行', true);
                    setTimeout(function () {
                        a.remove()
                    }, 2000)
                    return;
                }
                this.showkeyfield = true;
                this.model = Object.assign({}, this.selection); // @ 非常重要,新建对象取消与网格的绑定
                this.title = '修改行';
                this.$refs.dlg.open();
            },
            ok() {
                for (var item in this.rules) {
                    console.log(item,this.model[item]);
                    if (this.model[item] == null||this.model[item]=='') {
                        this.prompt.error(`${item} 值未填`);
                        return;
                    }
                }
                if (this.showkeyfield == true) {
                    // @ 修改行
                    this.p.data = [];
                    let row = {};
                    row._action = 'update';
                    row = Object.assign(row, this.model);
                    //row._reloadrow = 1;
                    this.p.data.push(row);

                    this.$axios.post('/crudProducts', this.p)
                        .then(res => {
                            if (res.data[0][0]._error == '') {
                                // step 修改网格数据
                                const index = this.data.indexOf(this.selection);
                                this.data.splice(index, 1, this.model);
                                // @ 将修改的数据重新给 selection
                                //console.log(this.data.selection,this.data[index]);
                                // this.data.selection = this.data[index];
                                this.$refs.dg.selectRow(this.model);
                                this.prompt.success('修改成功')
                                this.$refs.dlg.close();
                            }
                        })
                        .catch(err => {
                            console.error(err);
                        })
                } else {
                    // @ 新增行
                    this.p.data = [];
                    let row = {};
                    row._action = 'add';
                    row = Object.assign(row, this.model);
                    //row._reloadrow = 1;
                    this.p.data.push(row);

                    // step 查找新增的编号
                    myRunSqlProcedure('ifid', {
                        productid: row.productid
                    }, (results) => {
                        if (results[0].flag == 1) {
                            this.prompt.error('Productid 重复');
                            this.$refs.productid.focus();
                            return;
                        } else {
                            this.$axios.post('/crudProducts', this.p)
                                .then(res => {
                                    if (res.data[0][0]._error == '') {
                                        let index = 0;
                                        let id = row.productid;
                                        this.data.forEach(item => {
                                            if (item.productid < id) index++;
                                        })
                                        console.log(index, id);
                                        this.data.splice(index, 1, this.model);
                                        this.$refs.dg.selectRow(this.model);
                                        this.prompt.success('新增成功');
                                        if (this.showkeyfield == true) this.showkeyfield = false;
                                        this.$refs.dlg.close();
                                    }
                                })
                                .catch(err => {
                                    console.error(err);
                                })
                        }
                    });
                }
            },
            cancel() {
                if (this.showkeyfield == true) this.showkeyfield = false;
                this.$refs.dlg.close();
            }
        },
        created() {
            console.log('麻了,真的麻了');
            this.$axios.get('/products')
                .then(res => {
                    console.log(res.data[0]);
                    this.data = res.data;
                    this.getCategories(res.data);
                })
                .catch(err => {
                    console.error(err);
                })
        },
        mounted() {
            //this.$refs.dlg.open();
            this.prompt = LoadPrompt();
            var m = this.prompt.inform('这只是个测试', true);
            setTimeout(() => {
                m.remove();
            }, 2000);
        },
    }
</script>

⭐总结

 <DataGrid :data="data" style="height:100%;" :border="false" 
 :columnResizing="true" :multiSort="true" :filterable="filter" ref="dg" 
 selectionMode="single" @selectionChange="selection=$event">

<GridColumn field="categoryid" title="商品类别" width="80" align="center" :frozen="true"
    :sortable="true">
    <template slot="filter">
        <ComboBox class="f-full" :data="categories" :editable="false"
            :inputStyle="{textAlign:'center'}"
            @valueChange="$refs.dg.doFilter({field:'categoryid',op:'equal',value:$event.currentValue})">
        </ComboBox>
    </template>
</GridColumn>

    data.forEach((item) => {
                    if (this.categories.findIndex(value => value.value == item.categoryid) == -1) {
                        var t = {
                            value: item.categoryid,
                            text: item.categoryid
                        }
                        this.categories.push(t);
                        this.dlgcategories.push(t);
                    }
        })

this.model = Object.assign({}, this.selection);
==/非常重要,新建对象取消与网格的绑定

myFunctions.js

const axios = require('axios');

function myRunSqlProcedure(name,params,callback){
    let p = {};
    p.name = name;
    p.params = params;
    axios.post('/myRunSqlProcedure', p).then(res => {
        if(res.data[0]!=undefined)
        callback(res.data[0]); // @ 返回data数组
    })
    .catch(err => {
        console.error(err);
    })
}

exports.myRunSqlProcedure = myRunSqlProcedure;

😜 Vue 实战 1

👀 演示效果

Pasted image 20211122020801.png

vue3 不使用根标签报错提示

# Vue:The template root requires exactly one element.的解决办法

# 关于 vue3 不使用根标签报错提示 [vue/no-multiple-template-root] The template root requires exactly one element

Pasted image 20211121200504.png

ESlint 烦人问题解决

vscode两步自动配置eslint,_不忘初心-CSDN博客

安装 normalize.css

npm install normalize.css --save

帮助我们统一不同浏览器之间的显示差异

Docker 样式编写

Pasted image 20211122001154.png

rem 与 px 的转化关系

1rem = html fontsize
2rem = 2 * html fontsize

Flex 布局语法

https://www.runoob.com/w3cnote/flex-grammar.html

CSS3 box-sizing

https://www.runoob.com/css3/css3-box-sizing.html Pasted image 20211121225840.png

使用 iconfont

Pasted image 20211121230829.png Pasted image 20211121230818.png

使用 transform 缩小字体

浏览器显示字体的最小大小是 12px,我们可以用 transform 来显示 10px字体

  .docker__title{
    font-size : 20px;
    transform : scale(.5,.5);
    transform-origin: center top;
  }

⭐ scss

<style lang="scss">
  .docker {
    display: flex;
    box-sizing: border-box;
    position: absolute;
    padding: 0 .18rem;
    left: 0;
    bottom: 0;
    width: 100%;
    height: 0.49rem;
    border-top: 1px solid #F1F1F1;

    // background-color: indianred;
    &__item {
      flex: 1;

      .iconfont {
        margin: .07rem 0 .02rem 0;
        font-size: .18rem;
      }

      &--active {
        color: #1FA4FC;
      }
    }
    &__title{
    font-size: 20px;
    // @ 显示 10px 大小字体
    transform: scale(.5, .5);
    transform-origin: center top;
    }
  }

</style>

html

  <div class="docker">
    <span class="docker__item docker__item--active">
      <div class="iconfont">&#xe696;</div>
      <div class="docker__title">首页</div>
    </span>
    <span class="docker__item">
      <div class="iconfont">&#xe60c;</div>
      <div class="docker__title">购物车</div>
    </span>
    <span class="docker__item">
      <div class="iconfont">&#xe626;</div>
      <div class="docker__title">订单</div>
    </span>
    <span class="docker__item">
      <div class="iconfont">&#xe70b;</div>
      <div class="docker__title">我的</div>
    </span>
  </div>

BEM CSS 命名规则

block 块 (docker)__element 元素 (item)--Modifier 修饰器 (active)

统一样式文件加载

@import './base.scss';
@import './iconfont.css';
//main.js
import './style/index.scss'

line-height、height、font-size

https://blog.csdn.net/weixin_43109549/article/details/100387545

scss组织区域布局

Pasted image 20211122001217.png

    <div class="position">
      <span class="iconfont position__icon">&#xe7f1;</span>
      浙江理工大学学林街生活2区菜鸟驿站
      <span class="iconfont position__notice">&#xe601;</span>
      </div>

⭐ scss 与使用技巧

<style lang="scss">
@import './style/viriables.scss';
@import './style/mixins.scss';
  .wrapper{
    position:absolute;
    left:0;
    top:0;
    bottom: .5rem;
    right: 0;
    padding: 0 .18rem;
  }
  .position{
    color: $content-fontColor;
    position: relative;
    padding: .16rem .24rem .16rem 0;
    line-height: .22rem;
    font-size : .16rem;
    text-align: left; // @ body 里是 center
    @include ellipse;
    .position__icon{
      position:relative;
      top: .01rem;
      font-size: .2rem;
    }
    .position__notice{
      position:absolute;
      right:0;
      top: .17rem;
      font-size:.2rem;
    }
  }
  .docker {
    display: flex;
    box-sizing: border-box;
    position: absolute;
    padding: 0 .18rem;
    left: 0;
    bottom: 0;
    width: 100%;
    height: 0.49rem;
    border-top: .01rem solid #F1F1F1;
    color: $content-fontColor;
    &__item {
      flex: 1;

      .iconfont {
        margin: .07rem 0 .02rem 0;
        font-size: .18rem;
      }

      &--active {
        color: #1FA4FC;
      }
    }
    &__title{
    font-size: .2rem;
    // @ 显示 10px 大小字体
    transform: scale(.5, .5);
    transform-origin: center top;
    }
  }

</style>
@mixin ellipse{
    white-space: nowrap;
    text-overflow: ellipsis;
    overflow:hidden;    
}
$content-fontColor : #333;

scss 实现搜索及banner

Pasted image 20211122010207.png

  <div class="search">
    <span class="iconfont">&#xe699;</span>
    <span class="search__text">山姆会员商店优惠商品</span>
  </div>
  <div class="banner">
    <img class="banner__img" src="https://xiaonenglife.oss-cn-hangzhou.aliyuncs.com/projects/1121jingdong/banner.jpeg" alt=""/>
  </div>

⭐ scss (解决抖动关键技术)

  .search{
    margin-bottom: .12rem;
    line-height: .32rem;
    background: #F5F5F5;
    color : #b7b7b7;
    border-radius: .16rem;
    font-size: .14rem;
    .iconfont {
      position:relative;
      top: .02rem;
      font-size: .2rem;
      display:inline-block;
      padding: 0 .06rem 0 .12rem;
    }
    &__text{
      display: inline-block;
    }
  }
  .banner{
    // step 抖动效果没有了 // 关键技术
    height:0;
    overflow:hidden;
    padding-bottom:31.25%;
    border-radius: .1rem;
    &__img{
      width: 100%; // @ 让图片适配屏幕宽度
    }
  }
高度为0的banner块,定义img标签宽度为100%,我们只需要给padding-bottom预留图片高宽比,给banner块特定大小的张开,即可解决抖动问题

icons

Pasted image 20211122013244.png

  <div class="icons">
    <div class="icons__item">
      <img class="icons__item__img" src="https://xiaonenglife.oss-cn-hangzhou.aliyuncs.com/projects/1121jingdong/f1.png" alt="">
      <p class="icons__item__desc">应用</p>
    </div>
  </div>
  <div class="gap"></div>

scss

  .icons{
    display:flex;
    flex-wrap: wrap;
    margin-top: .16rem;
    &__item {
      width:20%;
      &__img {
        display: block;
        margin: 0 auto;
        border-radius: 50%;
        height: .4rem;
        width: .4rem;
      }
      &__desc{
        margin: .06rem 0 .16rem 0;
        text-align:center;
        color: $content-fontColor;
      }
    }
  }
  .gap{
    opacity: 80%;
    margin: 0 -.18rem;
    height : .1rem;
    background-color: $content-bgColor;
  }

nearby

Pasted image 20211122021003.png

 <div class="nearby">
        <h3 class="nearby__title">附近店铺</h3>
        <div class="nearby__item">
          <img class="nearby__item__img" src="https://xiaonenglife.oss-cn-hangzhou.aliyuncs.com/projects/1121jingdong/f11.png" />
          <div class="nearby__content">
              <div class="nearby__content__title">沃尔玛</div>
              <div class="nearby__content__tags">
                <span class="nearby__content__tag">月售1万+</span>
                <span class="nearby__content__tag">月售1万+</span>
                <span class="nearby__content__tag">月售1万+</span>
              </div>
              <p class="nearby__content__hightlight">VIP尊享满89元减4元运费卷(每月三张)</p>
          </div>
        </div>
  .nearby{
    &__title{
      margin-top:.16rem 0 .02em 0;
      font-size : .18rem;
      font-weight : normal;
      color : $content-fontColor;
    }
    &__item{
      display:flex;
      //@ 小心margin重叠 margin-top:.12 rem;
      padding-top:.12rem;
      &__img{
        margin-right: .16rem;
        width:.56rem;
        height:.56rem;
      }
    }
    &__content{
      padding-bottom:.12rem;
      border-bottom:1px solid $content-bgColor;
      flex: 1;
      &__title{
        line-height: .22rem;
        font-size: .16rem;
      }
      &__tags{
        margin-top: .08rem;
        line-height: .18rem;
        font-size: .13rem;
        color: $content-fontColor;
      }
      &__tag{
          margin-right:.16rem;
        }
      &__hightlight{
          margin: .08rem 0 0 0;  //p标签自带 13px margin
          line-height: .18rem;
          font-size: .13rem;
          color:red;
      }
    }
  }

🍔 完整代码

<template>
  <div class="wrapper">
    <div class="position">
      <span class="iconfont position__icon">&#xe7f1;</span>
      浙江理工大学学林街生活2区菜鸟驿站
      <span class="iconfont position__notice">&#xe601;</span>
      </div>
      <div class="search">
        <span class="iconfont">&#xe699;</span>
        <span class="search__text">山姆会员商店优惠商品</span>
      </div>
      <div class="banner">
        <img class="banner__img" src="https://xiaonenglife.oss-cn-hangzhou.aliyuncs.com/projects/1121jingdong/banner.jpeg" alt=""/>
      </div>
      <div class="icons">
        <div class="icons__item">
          <img class="icons__item__img" src="https://xiaonenglife.oss-cn-hangzhou.aliyuncs.com/projects/1121jingdong/f1.png" alt="">
          <p class="icons__item__desc">应用</p>
        </div>
        <div class="icons__item">
          <img class="icons__item__img" src="https://xiaonenglife.oss-cn-hangzhou.aliyuncs.com/projects/1121jingdong/f2.png" alt="">
          <p class="icons__item__desc">应用</p>
        </div>
        <div class="icons__item">
          <img class="icons__item__img" src="https://xiaonenglife.oss-cn-hangzhou.aliyuncs.com/projects/1121jingdong/f3.png" alt="">
          <p class="icons__item__desc">应用</p>
        </div>
        <div class="icons__item">
          <img class="icons__item__img" src="https://xiaonenglife.oss-cn-hangzhou.aliyuncs.com/projects/1121jingdong/f4.png" alt="">
          <p class="icons__item__desc">应用</p>
        </div>
        <div class="icons__item">
          <img class="icons__item__img" src="https://xiaonenglife.oss-cn-hangzhou.aliyuncs.com/projects/1121jingdong/f5.png" alt="">
          <p class="icons__item__desc">应用</p>
        </div>
        <div class="icons__item">
          <img class="icons__item__img" src="https://xiaonenglife.oss-cn-hangzhou.aliyuncs.com/projects/1121jingdong/f6.png" alt="">
          <p class="icons__item__desc">应用</p>
        </div>
        <div class="icons__item">
          <img class="icons__item__img" src="https://xiaonenglife.oss-cn-hangzhou.aliyuncs.com/projects/1121jingdong/f7.png" alt="">
          <p class="icons__item__desc">应用</p>
        </div>
        <div class="icons__item">
          <img class="icons__item__img" src="https://xiaonenglife.oss-cn-hangzhou.aliyuncs.com/projects/1121jingdong/f8.png" alt="">
          <p class="icons__item__desc">应用</p>
        </div>
        <div class="icons__item">
          <img class="icons__item__img" src="https://xiaonenglife.oss-cn-hangzhou.aliyuncs.com/projects/1121jingdong/f9.png" alt="">
          <p class="icons__item__desc">应用</p>
        </div>
        <div class="icons__item">
          <img class="icons__item__img" src="https://xiaonenglife.oss-cn-hangzhou.aliyuncs.com/projects/1121jingdong/f10.png" alt="">
          <p class="icons__item__desc">应用</p>
        </div>
      </div>
      <div class="gap"></div>
      <div class="nearby">
        <h3 class="nearby__title">附近店铺</h3>
        <div class="nearby__item">
          <img class="nearby__item__img" src="https://xiaonenglife.oss-cn-hangzhou.aliyuncs.com/projects/1121jingdong/f11.png" />
          <div class="nearby__content">
              <div class="nearby__content__title">沃尔玛</div>
              <div class="nearby__content__tags">
                <span class="nearby__content__tag">月售1万+</span>
                <span class="nearby__content__tag">月售1万+</span>
                <span class="nearby__content__tag">月售1万+</span>
              </div>
              <p class="nearby__content__hightlight">VIP尊享满89元减4元运费卷(每月三张)</p>
          </div>
        </div>
        <div class="nearby__item">
          <img class="nearby__item__img" src="https://xiaonenglife.oss-cn-hangzhou.aliyuncs.com/projects/1121jingdong/f11.png" />
          <div class="nearby__content">
              <div class="nearby__content__title">沃尔玛</div>
              <div class="nearby__content__tags">
                <span class="nearby__content__tag">月售1万+</span>
                <span class="nearby__content__tag">月售1万+</span>
                <span class="nearby__content__tag">月售1万+</span>
              </div>
              <p class="nearby__content__hightlight">VIP尊享满89元减4元运费卷(每月三张)</p>
          </div>
        </div>
        <div class="nearby__item">
          <img class="nearby__item__img" src="https://xiaonenglife.oss-cn-hangzhou.aliyuncs.com/projects/1121jingdong/f11.png" />
          <div class="nearby__content">
              <div class="nearby__content__title">沃尔玛</div>
              <div class="nearby__content__tags">
                <span class="nearby__content__tag">月售1万+</span>
                <span class="nearby__content__tag">月售1万+</span>
                <span class="nearby__content__tag">月售1万+</span>
              </div>
              <p class="nearby__content__hightlight">VIP尊享满89元减4元运费卷(每月三张)</p>
          </div>
        </div>
        <div class="nearby__item">
          <img class="nearby__item__img" src="https://xiaonenglife.oss-cn-hangzhou.aliyuncs.com/projects/1121jingdong/f11.png" />
          <div class="nearby__content">
              <div class="nearby__content__title">沃尔玛</div>
              <div class="nearby__content__tags">
                <span class="nearby__content__tag">月售1万+</span>
                <span class="nearby__content__tag">月售1万+</span>
                <span class="nearby__content__tag">月售1万+</span>
              </div>
              <p class="nearby__content__hightlight">VIP尊享满89元减4元运费卷(每月三张)</p>
          </div>
        </div>
      </div>
  </div>
  <div class="docker">
    <span class="docker__item docker__item--active">
      <div class="iconfont">&#xe696;</div>
      <div class="docker__title">首页</div>
    </span>
    <span class="docker__item">
      <div class="iconfont">&#xe60c;</div>
      <div class="docker__title">购物车</div>
    </span>
    <span class="docker__item">
      <div class="iconfont">&#xe626;</div>
      <div class="docker__title">订单</div>
    </span>
    <span class="docker__item">
      <div class="iconfont">&#xe70b;</div>
      <div class="docker__title">我的</div>
    </span>
  </div>
</template>

// BEM CSS 命名规则
// block 块 (docker)__element 元素 (item)--Modifier 修饰器 (active)

<style lang="scss">
@import './style/viriables.scss';
@import './style/mixins.scss';
  .wrapper{
    overflow-y:auto;
    position:absolute;
    left:0;
    top:0;
    bottom: .5rem;
    right: 0;
    padding: 0 .18rem .1rem .18rem;
  }
  .position{
    color: $content-fontColor;
    position: relative;
    padding: .16rem .24rem .16rem 0;
    line-height: .22rem;
    font-size : .16rem;
    text-align: left; // @ body 里是 center
    @include ellipse;
    .position__icon{
      position:relative;
      top: .01rem;
      font-size: .2rem;
    }
    .position__notice{
      position:absolute;
      right:0;
      top: .17rem;
      font-size:.2rem;
    }
  }
  .search{
    margin-bottom: .12rem;
    line-height: .32rem;
    background: #F5F5F5;
    color : #b7b7b7;
    border-radius: .16rem;
    font-size: .14rem;
    .iconfont {
      position:relative;
      top: .02rem;
      font-size: .2rem;
      display:inline-block;
      padding: 0 .06rem 0 .12rem;
    }
    &__text{
      display: inline-block;
    }
  }
  .banner{
    // step 抖动效果没有了
    height:0;
    overflow:hidden;
    padding-bottom:31.25%;
    border-radius: .1rem;
    &__img{
      width: 100%; // @ 让图片适配屏幕宽度
    }
  }
  .icons{
    display:flex;
    flex-wrap: wrap;
    margin-top: .16rem;
    &__item {
      width:20%;
      &__img {
        display: block;
        margin: 0 auto;
        border-radius: 50%;
        height: .4rem;
        width: .4rem;
      }
      &__desc{
        margin: .06rem 0 .16rem 0;
        text-align:center;
        color: $content-fontColor;
      }
    }
  }
  .gap{
    opacity: 80%;
    margin: 0 -.18rem;
    height : .1rem;
    background-color: $content-bgColor;
  }
  .nearby{
    &__title{
      margin-top:.16rem 0 .02em 0;
      font-size : .18rem;
      font-weight : normal;
      color : $content-fontColor;
    }
    &__item{
      display:flex;
      //@ 小心margin重叠 margin-top:.12 rem;
      padding-top:.12rem;
      &__img{
        margin-right: .16rem;
        width:.56rem;
        height:.56rem;
      }
    }
    &__content{
      padding-bottom:.12rem;
      border-bottom:1px solid $content-bgColor;
      flex: 1;
      &__title{
        line-height: .22rem;
        font-size: .16rem;
      }
      &__tags{
        margin-top: .08rem;
        line-height: .18rem;
        font-size: .13rem;
        color: $content-fontColor;
      }
      &__tag{
          margin-right:.16rem;
        }
      &__hightlight{
          margin: .08rem 0 0 0;  //p标签自带 13px margin
          line-height: .18rem;
          font-size: .13rem;
          color:red;
      }
    }
  }
  .docker {
    text-align: center;
    display: flex;
    box-sizing: border-box;
    position: absolute;
    padding: 0 .18rem;
    left: 0;
    bottom: 0;
    width: 100%;
    height: 0.49rem;
    border-top: .01rem solid $content-bgColor;
    color: $content-fontColor;
    &__item {
      flex: 1;

      .iconfont {
        margin: .07rem 0 .02rem 0;
        font-size: .18rem;
      }

      &--active {
        color: #1FA4FC;
      }
    }
    &__title{
    font-size: .2rem;
    // @ 显示 10px 大小字体
    transform: scale(.5, .5);
    transform-origin: center top;
    }
  }

</style>
@mixin ellipse{
    white-space: nowrap;
    text-overflow: ellipsis;
    overflow:hidden;    
}
$content-fontColor : #333;

⭐⭐ 首页组件的合理拆分

Pasted image 20211122025438.png Pasted image 20211122025447.png

⭐⭐ 精简冗余代码

问题代码

Pasted image 20211122030002.png Pasted image 20211122030012.png

解决办法

Pasted image 20211122030056.png

不转义,以html形式展现

Pasted image 20211122030410.png

使用item.index判断CSS样式
<template>
  <div class="docker">
    <span :class="{'docker__item':true,'docker__item--active':index==0}"  v-for="(item,index) in dockerList"  :key="item.icon">
      <div class="iconfont" v-html="item.icon"></div>
      <div class="docker__title">{{item.text}}</div>
    </span>
  </div>
</template>
<script>
export default {
  name: 'Docker',
  setup () {
    const dockerList = [
      { icon: '&#xe696;', text: '首页' },
      { icon: '&#xe60c;', text: '购物车' },
      { icon: '&#xe626;', text: '订单' },
      { icon: '&#xe70b;', text: '我的' }
    ]
    return { dockerList }
  }
}
</script>

简化nearby

Pasted image 20211122031804.png

简化icons

Pasted image 20211122031748.png

CSS 作用域约束

一个组件的样式只对这个组件生效
解决办法:加scoped
Pasted image 20211122032217.png

Vue 开发者工具

Pasted image 20211122033207.png Pasted image 20211122033717.png

😜 Vue + Node.JS + EasyUI ②

📕 问题资料

解决直接修改父组件数据出错

Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "grants" (found in component

https://blog.csdn.net/l960516/article/details/99578604

https://blog.csdn.net/github_35549695/article/details/82770044

EASYUI-ICONS

Pasted image 20211125010350.png https://blog.csdn.net/aas3637721/article/details/92980146

slot与slot-scope

深入理解vue中的slot与slot-scope

JS 打开新窗口

Pasted image 20211125010502.png https://www.cnblogs.com/liumengdie/p/7918601.html

⭐this.$nextTick()

在created()钩子函数执行的时候DOM 其实并未进行任何渲染,而此时进行DOM操作并无作用,而在created()里使用this.$nextTick()可以等待dom生成以后再来获取dom对象

this.$nextTick()将回调延迟到下次 DOM 更新循环之后执行。在修改数据之后立即使用它,然后等待 DOM 更新。 Pasted image 20211125010652.png

父子组件通信、嵌套组件事件传递

https://blog.csdn.net/weixin_41993525/article/details/109404120

父组件中获取子组件中的数据

https://www.cnblogs.com/beileixinqing/p/7597647.html

JS数组遍历的几种方法

https://blog.csdn.net/fu983531588/article/details/89597521

⭐ HTTP multipart/form-data

https://blog.csdn.net/qq_33706382/article/details/78168325

⭐ express上传文件大小受限

https://blog.csdn.net/u010186511/article/details/78113207 Pasted image 20211122050748.png

🌏 myFilebox(多组件)

👀 演示效果

Pasted image 20211125005050.png

⏰ 开发时长:8 小时

⭐ Multer Module

npm install multer --save

https://github.com/expressjs/multer/blob/master/doc/README-zh-cn.md https://blog.csdn.net/yun_hou/article/details/96869219

后端开发

index.js 修改

const express = require('express')
const app = express();
const sqlRouter = require('./routers/sql');
const uploadRouter = require('./routers/upload');
const path = require('path');

const port = 3000;

// @ 请求处理函数
app.use(express.urlencoded({extended:false,limit: '50mb'}));
app.use(express.json({limit: '50mb'}));

// step 文件上传静态资源目录
app.use('/files',express.static(path.join(__dirname,'uploads')));

// @ 配置路由
app.use(sqlRouter);
app.use(uploadRouter);

// @ 监听端口
app.listen(port,()=>{
    console.log('running...');
});

upload.js 路由模块

const express = require('express');
const router = express.Router();
const service = require('../services/upload');
const multer = require('multer');
const path = require('path');

let upload = multer({
  storage: multer.diskStorage({
      destination: function (req, file, cb) {
          cb(null, path.join(__dirname,'../uploads/'));
      },
      filename: function (req, file, cb) {
          var changedName = (new Date().getTime())+'_'+file.originalname;
          cb(null, changedName);
      }
  })
});

// @ 上传多个文件
router.post('/receiveFiles', upload.array('myFiles'),service.receiveFiles);

// @ 下载单个文件
router.post('/downloadFile', service.downloadFile);

module.exports = router;

upload.js 业务模块

const path = require('path');
const fs = require('fs');
const dest = `http://localhost:3000/files/`;
//let uploadDir = '../uploads';

exports.receiveFiles = (req, res) => {
  // step 多文件处理
  let fileList = [];
  const files = req.files;
  // step 判断是否有文件,防止程序出错
  if (files == undefined) {
    res.json({
      '_error': '请求中没有任何文件'
    });
    return;
  }
  const len = files.length;
  for (let i = 0; i < len; i++) {
    const f = files[i];
    let file = {
      id: i + 1,
      size: f.size,
      ext: f.originalname.substring(f.originalname.lastIndexOf('.')+1,f.originalname.length),
      name: f.originalname,
      path: dest + f.filename //req.file.path
    }
    fileList.push(file);
  }
  res.json({
    '_error': '',
    'count': fileList.length,
    'flag': 1,
    'fileList': fileList
  })
}

exports.downloadFile = (req, res) => {
  let body = req.body;
  let filename = body.filename;
  let source = path.join(__dirname, '../uploads', filename);
  // step 判断文件是否存在
  fs.stat(source, (err, stat) => {
    if (err) {
      //console.log(err);
      res.json({
        '_error': `${filename} 不存在`
      });
      return;
    }
    res.download(source,filename.substring(filename.indexOf('_')+1,filename.length));
  })
}

接口测试

Pasted image 20211125004329.png Pasted image 20211125004302.png

前端开发

fb_button.vue

<template>
  <div>
    <ButtonGroup>
      <FileButton style="width:100px" accept="*" :multiple="true" @select="onFileSelect($event)">选择文件
      </FileButton>
      <LinkButton @click="clearFiles" style="width:100px">清除列表</LinkButton>
    </ButtonGroup>
    <LinkButton @click="uploadFiles" style="width:60px;margin-left:20px;">上传</LinkButton>
  </div>
</template>
<script>
  import myFun from '../myFunctions.js';

  export default {
    name: 'FileboxButton',
    //props:['xfiles'],
    data() {
      return {
        files: [],
        results : {}
      }
    },
    computed:{
      files_:{
        get(){
          return this.files;
        },
        set(val){
          this.$emit('updatefiles',val);  
        }
      }
    },
    methods: {
      onFileSelect(e) {
        e.forEach(file => {
          file.sizekb = getKB(file);
          file.url = window.URL.createObjectURL(file); // @ 生成本地地址
          //this.files.push(file); 方法 1
        });
        this.files = this.files.concat(e); // 方法 2
        console.log(this.files);
        this.$emit('updatebeforefiles',this.files);  
      },
      uploadFiles() {
        var formData = new FormData();
        // warn 此处文件名必须为 myFiles ,因为后台设置仅接口此文件名
        this.files.forEach(file=>{
            formData.append('myFiles', file);
        })
        myFun.axiosPost('/receiveFiles', formData, (results) => {
          this.results = results;
          console.log(results);          
          this.files_ = this.results.fileList; // @ 触发计算属性的computed的set更新父组件数据,也可以不用这种方法,用下面的方法
          this.$emit('updateresult',results);  
        })
      },
      clearFiles(){
        this.$emit('updatebeforefiles',[]);  
      }
    },
    mounted() {
      myFun.test(this.$options.name);
    }
  }

  function getKB(file) {
    return (file.size / 1024).toFixed(2);
  }

  function getMB(file) {
    return (file.size / 1024 / 1024).toFixed(2);
  }

</script>

fb_result.vue

<template>
  <div>
    <span v-if="result._error==''&&result.fileList.length>0&&result.flag==1">
      上传成功(共{{result.fileList.length}}个文件)
    </span>
  </div>
</template>
<script>
export default {
   props:['result'],
   name:'fileboxResult'
}
</script>

<style lang="scss" scoped>
  span{
    color:red;
  }
</style>

fb_beforeshow.vue

<template>
  <div>
    <DataList :data="files" :border="false">
      <template slot-scope="{row}">
        <div>
          <!--   <img height="64" width="64" src="row.url"> -->
          <LinkButton style="float:right;margin:4px;" iconCls="icon-clear" :plain="true" @click="removeFile(row)"></LinkButton>
          <LinkButton style="float:right;margin:4px;" iconCls="icon-search" :plain="true" @click="previewLocalFile(row)"></LinkButton>          
          <div class="name">名称:{{row.name}}</div>
          <div class="size">大小:{{row.sizekb}} KB</div>
        </div>
      </template>
    </DataList>
  </div>
</template>
<script>
  export default {
    name: 'fileboxBeforeShow',
    props: ['files'],
    methods: {
      removeFile(row){
        const index = this.files.indexOf(row);
        this.$emit('updatebeforefiles',this.files.splice(index,1));  
      },
      previewLocalFile(row){
        window.open(row.url,'_blank');
      }
    },
  }
</script>

<style lang="scss" scoped>
  .name{
    width:300px;
    white-space: nowrap;
    text-overflow: ellipsis;
    overflow:hidden;  
  }
</style>

fb_show.vue

<template>
  <div>
    <DataList :data="files" :border="false">
      <template slot-scope="{row}">
        <div>
          <!--   <img height="64" width="64" src="row.url"> -->
          <LinkButton style="float:right;margin:4px;" iconCls="icon-clear" :plain="true" @click="removeFile(row)"></LinkButton>
          <LinkButton style="float:right;margin:4px;" iconCls="icon-search" :plain="true" @click="previewLocalFile(row)"></LinkButton>          
          <div class="name">名称:{{row.name}}</div>
          <div class="size">大小:{{(row.size / 1024).toFixed(2)}} KB</div>
        </div>
      </template>
    </DataList>
  </div>
</template>
<script>
  export default {
    name: 'FileboxShow',
    props: ['files', 'showstyle','blank'],
    methods: {
      removeFile(row) {
        const index = this.files.indexOf(row);
        this.$emit('updatebeforefiles', this.files.splice(index, 1));
      },
      previewLocalFile(row) {
        if(this.blank)
        window.open(row.path, '_blank');
        else
        this.$emit('addtab',row);
      }
    }
  }
</script>

Tabs_filebox.vue

<template>
  <div style="overflow:none;">
    <Tabs ref="mytabs" :scrollable="true" @tabClose="del"
      style="position:absolute;top:0;left:0;right:0;bottom:0;overflow:none;">
      <TabPanel title="文件管理" iconCls="icon-ok" :bodyStyle="{overflow:'hidden'}">
        <Filebox @addtab="add" :blank="false" style="margin:20px;"></Filebox> 
        /* -------------------------------------标签页管理-------------------------------- */<br>
        <Tagbox /> 
      </TabPanel>
      <TabPanel v-for="tab in panels" :key="tab.id" :title="tab.title" :closable="true"
        :bodyStyle="{overflow:'hidden'}">
        <!-- <div v-html="tab.content"></div> -->
        <iframe v-if="tab.ext=='pdf'" :src="tab.url" style="width:100%;height:100%;" scroll="true"></iframe>
        <audio v-else-if="'mp3;wav'.indexOf(tab.ext)>=0" :src="tab.url" controls="true"
          style="margin:20px;width:300px;"></audio>
        <img v-else-if="'jpg;jpeg;gif;tif;tiff;png'.indexOf(tab.ext)>=0" :src="tab.url" />
        <iframe v-else :src="tab.url"></iframe>
      </TabPanel>
    </Tabs>
  </div>
</template>
<script>
  import Filebox from './Filebox'
  import Tagbox from './tagbox'
  //import MyCombobox from './myCombobox.vue';

  export default {
    name: 'TabsFileBox',
    components: {
    Filebox,
    Tagbox,
    //MyCombobox
},
    data() {
      return {
        panels: [],
        form: {
          t1:null,
          t2:'hello'
        }
      }
    },
    methods: {
      // @ for ComboBox
      // updateForm(value,id){
      //   this.form[id] = value; 
      // },
      add(row) {
        let panel = {};
        panel.id = row.id;
        panel.title = row.name;
        panel.url = row.path;
        panel.ext = row.ext;
        //panel.content = `<font color="red">hahaha</font>`;
        let index = this.panels.findIndex(item => item.id == panel.id);

        if (index == -1) {
          this.panels.push(panel);
          this.$nextTick(() => { // warn
            console.log(this.panels.length);
            this.$refs.mytabs.select(this.panels.length);
          });
        } else {
          this.$refs.mytabs.select(index + 1);
        }
        console.log(this.panels);
      },
      del(panel) {
        //console.log(panel);
        // warn 官方提供的函数有bug
        //const index = this.$refs.mytabs.getPanelIndex(panel);

        // @ 解决方法
        const index = this.panels.findIndex(item => item.title == panel.title);
        this.panels.splice(index, 1);
      }
    },
    mounted(){
      setInterval(()=>{
        console.log('父组件数据 ',this.form.t1,' 子组件数据:',this.$refs.combo.value,this.form);
        //console.log('输出引用',this.$refs.combo);
      },10000);
    }
  }

</script>

🌏myTagbox(单组件)

👀 演示效果

Pasted image 20211125005108.png

存储过程

drop table if exists tagjson;
create table tagjson(
tags json
);
insert into tagjson values('{"tags":"[]"}');

select * from tagjson

drop procedure if exists gettags;
delimiter $$
create procedure gettags()
begin
select * from tagjson;
end $$
delimiter ;


drop procedure if exists savetags;
delimiter $$
create procedure savetags($tags json)
begin
update tagjson set tags = $tags;
end $$
delimiter ;

前端代码

<template>
  <div>
    <!-- @ 非常好的方法 || -->
    <Label v-if="opts.isLabel||opts.label!=undefined" :style="opts.labelStyle" :for="opts.id" :align="opts.labelPosition||'left'">{{opts.label}}</Label>
    <ComboBox :ref="opts.id" @valueChange="onChange" :style="opts.comboStyle" :textField="opts.textField"
      :valueField="opts.valueField" :inputId="opts.id" v-model="value" :data="data"></ComboBox>
    <p v-if="value!=''&&opts.show">选择的值: {{value}}</p>
  </div>
</template>

<script>
  import myFun from '../myFunctions.js';

  export default {
    props: ['opts'],
    name: 'myCombobox',
    data() {
      return {
        data: [], // fixme   this.getData(), 异步的时候有问题
        value: null
      }
    },
    methods: {
      onChange(values) {
        // step 这里还可以对 values 进行操作
        if (this.opts.emit != undefined)
          this.$emit(this.opts.emit, this.value, this.opts.id);
      },
      getData() {
        let data = [];
        // @ 这里更新数据 可以从api接口读取数据 也可以从items读取数据
        if (this.opts.items != undefined) {
          // @ 当 items 为js对象数组
          if (typeof (this.opts.items) == 'object') {
            this.opts.items.forEach(item => {
              var p = {};
              if (this.opts.valueField != undefined) {
                p[this.opts.valueField] = item[this.opts.valueField];
              } else p.value = item.value;

              if (this.opts.textField != undefined) {
                p[this.opts.textField] = item[this.opts.textField];
              } else p.text = item.text;

              data.push(p);
            })
          }
          // @ 当 items 为 字符串
          else if (typeof (this.opts.items) == 'string') {
            let tmp = this.opts.items.split(';');
            tmp.forEach(item => {
              var p = {};
              if (this.opts.valueField != undefined) {
                p[this.opts.valueField] = item;
              } else p.value = item;

              if (this.opts.textField != undefined) {
                p[this.opts.textField] = item;
              } else p.text = item;
              data.push(p);
            })
          }
        }
        // @ 当有api键值对时,从接口请求数据并将数据合并到data中

        if (this.opts.api != undefined && this.opts.api!='') {
          myFun.myRunSqlProcedure(this.opts.api,
            this.opts.params != undefined ? this.opts.params : [],
            (results) => {
              // @ 这里合并数据到 data
              if(results)
                data = data.concat(results);
              this.data = data;
              this.select(this.opts.select);
            })
        }
        // STEP 如果不需要从数据库读取直接返回,需要的话才需要异步等待数据再返回
        else {
          this.data = data;
          this.select(this.opts.select);
        }
      },
      select(i) {
        // STEP 设置初始选项
        if (typeof (i) == 'number')
          this.value = this.data[i][this.opts.valueField != undefined ? this.opts.valueField : 'value'];
        else if (typeof (i) == 'string')
          this.value = i;
        else
          this.value = null;

        // STEP 触发事件让父组件的对象更新,否则第一次自动选中父组件数据与子组件的不同步 !!!
        this.$emit(this.opts.emit, this.value, this.opts.id);
      }
    },
    created() {
      this.getData();
    },
    mounted() {
      myFun.test(this.$options.name);
    },
  }
</script>

🌏 myCombobox(单组件)

👀 演示效果

Pasted image 20211125005242.png

存储过程

drop procedure if exists comboTest;
delimiter $$
create procedure comboTest()
begin
select customerid as value,companyname as text from customers limit 5;
end $$
delimiter ;

call comboTest();

使用方式

<MyCombobox style="margin:20px;" :opts="{
  id:'t1',
  //labelPosition:'left',  
  label:'商品编码:',
  items:'测试;男;女',
  //textField:'name',
  //valueField:'gender',  
  //api:'comboTest',  // @ 从api获取数据合并
  //params:''
  show:true,  // @ debug show?
  //labelStyle:{},  // @ label 样式
  comboStyle:{width:'200px'}, //@ combo 样式
  emit:'form',  //@ 触发事件
  select:0 //'男'  //@ 选中第几项,或默认值
}" @form="updateForm" ref="combo"/>

🌏 myDialog

👀 演示效果

Pasted image 20211125005359.png

前端代码

<template>
  <div>
    这只是一个测试
    <!--

    -->
    <Dialog :ref="opts.id" bodyCls="f-column" :draggable="opts.draggable" :title="opts.title"
      :resizable="opts.resizable" :closed="opts.closed" :modal="opts.modal" :iconCls="opts.iconCls" @close="cancel"
      :dialogStyle="opts.style">
      <template slot="header">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
        <slot name="title"></slot>
      </template>
      <!--自定义title兼容html语言-->

      <div class="f-full" style="overflow:hidden">
        <Form :model="model" :rules="opts.rules" :labelWidth="opts.labelWidth||80"
          :labelAlign="opts.labelPosition||'right'" style="padding:15px 20px 20px 10px;">

          <FormField v-for="item in opts.fields" :name="item.id" :labelWidth="item.labelWidth"
            :labelAlign="item.labelPosition" :label="item.label" :key="item.id">
            <NumberBox v-if="item.type=='numberbox'" :precision="item.precision" :ref="item.id" :disabled="showkeyfield"
              v-model="model[item.id]"></NumberBox>

            <TextBox v-else-if="item.type=='textbox'"  :ref="item.id" v-model="model[item.id]"></TextBox>

            <MyCombobox v-else-if="item.type=='combobox'"  :opts="{
          id:item.id,
          //labelPosition:'left',  
          //isLabel:false,
          //label:'商品编码:',
          items:item.items,//'测试;男;女',
          show:false,  // @ debug show?
          comboStyle:item.style , //{width:'300px'}, //@ combo 样式
          api:item.api,
          params:item.params,
          select:item.select,
          emit:item.emit
        }"  :ref="item.id" @form="updateForm"></MyCombobox>

          </FormField>

          <FormField v-if="opts.isButton" style="float:right;">
            <LinkButton iconCls="icon-ok" @click="ok()" style="width:100px">保存</LinkButton>
            <LinkButton iconCls="icon-no" @click="cancel()" style="width:100px">关闭</LinkButton>
          </FormField>
        <slot name="content"></slot>

        </Form>
      </div>


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

<script>
  // step 父组件对子组件进行ref引用,然后子组件编写赋值方法,打开dialog的时候父组件调用子组件的赋值方法
  // this.$refs.xxx.open(selection);
  import MyCombobox from "./myCombobox";

  export default {
    name: 'myDialog',
    props: ['opts'], // @ 获取配置项
    components: {
    MyCombobox
    },
    computed:{
      showkeyfield(){
        if(this.addoredit=='add') return true;
        else return false;
      }
    },
    data() {
      return {
        model: {}, // test 子组件的表单数据
        addoredit: 'update'
      }
    },
    methods: {
      // @ mycombobox专用函数
      updateForm(value,id){
         this.model[id] = value; 
      },
      open(data,addoredit) {
        this.$refs[this.opts.id].open();
        // @ 取消引用
        this.model = Object.assign({}, data);
        if(addoredit!=undefined) this.addoredit = addoredit;
        console.log('open 事件被触发了');
      },
      cancel() {
        // step 暂时不需要 还原disabled ?
        // @ 会被执行两次,一次按钮事件,一次close事件
        console.log('窗口被关闭了');
        this.$refs[this.opts.id].close();
      },
      ok(){
        this.$emit(this.opts.emitok,this.model,this.addoredit);  
        this.$refs[this.opts.id].close();      
      }
    },
    created(){
      // @ 初始化一次
      this.opts.fields.forEach(item=>{
        this.model[item.id] = null;
      })
    }
  }

</script>

使用方式

Pasted image 20211125005345.png Pasted image 20211125005326.png

😜 Vue 实战 2

登录页面

👀 演示效果

Pasted image 20211125012054.png

Login.vue

<template>
  <div class="wrapper">
    <img class="wrapper__img" src="https://xiaonenglife.oss-cn-hangzhou.aliyuncs.com/projects/1121jingdong/f11.png"
      alt="">
    <div class="wrapper__input">
      <input class="wrapper__input__content" placeholder="请输入手机号"/>
    </div>
    <div class="wrapper__input">
      <input class="wrapper__input__content" type="password" placeholder="请输入密码"/>
    </div>
    <div class="wrapper__login-button" @click="handleLogin">登录</div>
    <div class="wrapper__login-link">立即注册</div>
  </div>
</template>
<script>
import router from '../../router'

export default {
  name: 'Login',
  setup (props) {
    const handleLogin = () => {
      localStorage.isLogin = true
      // step 路由跳转
      router.push({ name: 'Home' })
    }
    return { handleLogin }
  }
}

</script>
<style lang="scss" scoped>
@import '../../style/viriables.scss';
  .wrapper{
    position:absolute;
    top: 50%;
    left: 0;
    right: 0;
    transform: translateY(-50%);
    &__img{
      display:block;
      margin: 0 auto .4rem auto;
      width: .66rem;
      height: .66rem;
    }
    &__input{
      box-sizing: border-box;
      padding: 0 .16rem;
      height: .48rem;
      margin: 0 .4rem 0.16rem .4rem;
      background: #F9F9F9;
      border: 1px solid rgba(0,0,0,0.10);
      border-radius: 6px;
      &__content{
        line-height: .48rem;
        border:none;
        outline: none;
        width:100%;
        background:none;
        font-size: .16rem;
        // @ 底下没颜色最好别用 rgba 无意义
        color : $content-notice-fontColor;
        &::placeholder{
          color: $content-notice-fontColor;
        }
      }
    }
    &__login-button{
      line-height: .48rem;
      font-size: .16rem;
      text-align: center;
      margin: 0.32rem .4rem 0.16rem .4rem;
      background: #0091FF;
      box-shadow: 0 .04rem .08rem 0 rgba(0,145,255,0.32);
      border-radius: .04rem;
      border-radius: .04rem;
      color: #fff;
    }
    &__login-link{
      text-align: center;
      font-size: 14px;
      color: $content-notice-fontColor;
    }
  }
</style>

登录注册页面可以写一起去

但不方便拓展!

⭐路由守卫实现基础校验

import { createRouter, createWebHashHistory } from 'vue-router'
import Home from '../views/home/Home.vue'
import Login from '../views/login/Login.vue'

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/login',
    name: 'Login',
    component: Login,
    beforeEnter: (to, from, next) => {
      const { isLogin } = localStorage
      isLogin ? next({ name: 'Home' }) : next()
    }
  }
]

const router = createRouter({
  history: createWebHashHistory(),
  routes
})

router.beforeEach((to, from, next) => {
  const { isLogin } = localStorage;
  // @ to.name 防止死循环
  (isLogin || to.name === 'Login') ? next() : next({ name: 'Login' })
})

export default router

前端登录功能

假接口数据平台

https://www.fastmock.site/

myFun

/* eslint-disable */
const axios = require('axios');

axios.defaults.baseURL='/api'
axios.defaults.headers.post['Content-Type'] = 'application/json'

exports.test= (str)=>{
  console.log(`${str} 成功加载 MyFunctions`);
}

exports.myRunSqlProcedure = (name, params, callback) => {
  let p = {};
  p.name = name;
  p.params = params;
  exports.axiosPost('/myRunSqlProcedure',p,callback);
};

exports.axiosPost = (url, p, callback) => {
  axios.post(url, p).then(res => {
    if (res.data != undefined)
        if(res.data[0]!=undefined)
        callback(res.data[0]); // @ 返回data数组
        else
        callback(res.data);
    })
    .catch(err => {
      console.error(err);
    })
}

vue

import router from '../../router'
import myFun from '../../myFunctions'
import { reactive } from '@vue/reactivity'
export default {
  name: 'Login',
  setup (props) {
    const data = reactive({
      username: '',
      password: ''
    })
    const handleLogin = () => {
      // step 使用api验证
      myFun.myRunSqlProcedure('kms2020:studentLogin', data, (results) => {
        localStorage.isLogin = true
        // step 路由跳转
        router.push({ name: 'Home' })
      })
    }
    const handleRegisterClick = () => {
      router.push({ name: 'Register' })
    }
    return { handleLogin, handleRegisterClick, data }
  }
}

存储过程

use kms2020;
drop procedure if exists studentLogin; 
delimiter $$
create procedure studentLogin(
    $username varchar(100),
    $password varchar(100)
)
begin
    declare $s mediumtext;
    set $s=sys_toycode($password);
    select count(*) into @count from x_students where studentid = $username and password = $s;
    if(@count=0) then
        select 'failure' as message,1 _error;
    else
        select 'success' as message,0 _error,studentid,account,mobile,email,weixin,password from x_students where studentid = $username and password = $s;
    end if;
end $$
delimiter ;

call studentLogin('2020333504045','Abcdqw10086');
call studentLogin('2020333504045','Abcdqwsadasdasdasd10086');
call studentLogin('2020333504sadsda045','Abcdqwsadasdasdasd10086');

💡 Promise Async,Await

Pasted image 20211125045430.png https://www.youtube.com/watch?v=CTChl_DYTz0&t=1s https://www.youtube.com/watch?v=zoZiQJ38bXk

PROMISE

Promise 有效解决 callbackhell
Promise 提供 resolve reject 函数告知超长任务进度
Then 语句会在异步执行任务完成后被执行
Catch 语句捕捉错误

ASYNC AWAIT

让异步执行代码更贴近同步执行代码
概念由Promise基础上演变
Async function 会返回 Promise object
Await 关键字只能在 Async function 里使用
Await 关键字必须尾随 Promise 对象
Await 用 Try Catch 捕捉错误

JavaScript-变量简介

https://blog.csdn.net/weixin_39765100/article/details/110795506

⭐⭐ 空值判断写法

可选链、空值合并运算符、空值赋值运算符

https://blog.csdn.net/qq_44943717/article/details/109890133 https://blog.csdn.net/yun_master/article/details/115015113 https://blog.csdn.net/weixin_44164824/article/details/108693513 Pasted image 20211125054233.png Pasted image 20211125054656.png

⭐⭐ ! ! 写法 / 用 if(a)

Pasted image 20211125055203.png Pasted image 20211125055345.png https://blog.csdn.net/chenggang_zh/article/details/84335252

优化登录功能

使用 Async Await ?. 写法

    const data = reactive({
      username: '',
      password: ''
    })
    // important Async Await 写法
    const handleLogin = async () => {
      try {
        const result = await axios.post('myRunSqlProcedure', {
          name: 'kms2020:studentLogin',
          params: data
        })
        if (result?.data[0]?._error === 0) {
          localStorage.isLogin = true
          router.push({ name: 'Home' })
        } else {
          alert('失败')
        }
      } catch (E) {
        alert('请求失败')
      }
    }

封装Axios.post

/* eslint-disable */
import axios from 'axios';

export const post = (url, data = {}) => {
  return new Promise((resolve,reject)=>{
    axios.post(url,data,{
      baseURL:'/api',
      headers:{
        'Content-Type':'application/json'
      }
    }).then(res=>{
      resolve(res.data); // res.data
    },err=>{
      reject(err);
    })
  });
}

Pasted image 20211125061440.png

Toast 弹窗

Pasted image 20211125064500.png

Toast.vue

<template>
  <div class="toast">{{message}}</div>
</template>
<script>
export default {
  name: 'Toast',
  props: ['message']
}
</script>
<style lang="scss" scoped>
.toast {
  position: fixed;
  left: 50%;
  top: 50%;
  transform: translate(-50%,-50%);
  padding: .1rem;
  background: rgba(0,0,0,.35);
  border-radius: .05rem;
  color: #fff;
}
</style>

调用

const showToast = (toastMessage) => {
  data.showToast = true
  data.toastMessage = toastMessage
  setTimeout(() => {
    data.showToast = false
  }, 2000)
}

// important Async Await 写法
const handleLogin = async () => {
  try {
    const result = await post('myRunSqlProcedure', {
      name: 'kms2020:studentLogin',
      params: {
        username: data.username,
        password: data.password
      }
    })
    if (result[0]?._error === 0) {
      localStorage.isLogin = true
      router.push({ name: 'Home' })
    } else {
      showToast('登录失败')
      // alert('登录失败')
    }
  } catch (e) {
    showToast('请求失败')
  }
}

⭐ 拆分代码增加逻辑性

原本代码

import router from '../../router'
import { reactive } from '@vue/reactivity'
import { post } from '../../utils/request'
import Toast from '../../components/Toast'
// import axi os from 'axios'
// axios.defaults.baseURL = '/api'
// axios.defaults.headers.post['Content-Type'] = 'application/json'

export default {
  name: 'Login',
  components: {
    Toast
  },
  setup (props) {
    const data = reactive({
      username: '',
      password: '',
      showToast: false,
      toastMessage: ''
    })

    const showToast = (toastMessage) => {
      data.showToast = true
      data.toastMessage = toastMessage
      setTimeout(() => {
        data.showToast = false
      }, 2000)
    }

    // important Async Await 写法
    const handleLogin = async () => {
      try {
        const result = await post('myRunSqlProcedure', {
          name: 'kms2020:studentLogin',
          params: {
            username: data.username,
            password: data.password
          }
        })
        if (result[0]?._error === 0) {
          localStorage.isLogin = true
          router.push({ name: 'Home' })
        } else {
          showToast('登录失败')
          // alert('登录失败')
        }
      } catch (e) {
        showToast('请求失败')
      }
    }

    const handleRegisterClick = () => {
      router.push({ name: 'Register' })
    }
    return { handleLogin, handleRegisterClick, data }
  }
}

🍔 拆分方法

  • 系统级别的引入放到顶部,自己写的放到底下 Pasted image 20211125074804.png

  • 将不重要的逻辑拆离,setup只保留主流程

Login.vue

<script>
import { reactive } from '@vue/reactivity'
import { post } from '../../utils/request'
import router from '../../router'

import Toast, { useToastEffect } from '../../components/Toast'

export default {
  name: 'Login',
  components: {
    Toast
  },
  // @ 更关注主流程
  setup (props) {
    const data = reactive({ username: '', password: '' })
    const { toastData, showToast } = useToastEffect()
    // important Async Await 写法
    const handleLogin = async () => {
      try {
        const result = await post('myRunSqlProcedure', {
          name: 'kms2020:studentLogin',
          params: {
            username: data.username,
            password: data.password
          }
        })
        if (result[0]?._error === 0) {
          localStorage.isLogin = true
          router.push({ name: 'Home' })
        } else {
          showToast('登录失败')
          // alert('登录失败')
        }
      } catch (e) {
        showToast('请求失败')
      }
    }

    const handleRegisterClick = () => {
      router.push({ name: 'Register' })
    }
    return { handleLogin, handleRegisterClick, data, toastData }
  }
}

</script>

Toast.vue

<script>
import { reactive } from '@vue/reactivity'

export default {
  name: 'Toast',
  props: ['message']
}

// @ 关于Toast相关的逻辑(在其他组件里也要用,所以放到子组件里)
export const useToastEffect = () => {
  const toastData = reactive({
    showToast: false,
    toastMessage: ''
  })
  const showToast = (toastMessage) => {
    toastData.showToast = true
    toastData.toastMessage = toastMessage
    setTimeout(() => {
      toastData.showToast = false
    }, 2000)
  }
  return { toastData, showToast }
}

</script>

2021.11.25

😜 Vue 实战 3

修改路由(异步加载组件)

import { createRouter, createWebHashHistory } from 'vue-router'
const routes = [
  {
    path: '/',
    name: 'Home',
    component: () => import(/* webpackChunkName: "home" */ '../views/home/Home.vue')
  },
  {
    path: '/login',
    name: 'Login',
    component: () => import(/* webpackChunkName: "login" */ '../views/login/Login.vue'),
    beforeEnter: (to, from, next) => {
      const { isLogin } = localStorage
      isLogin ? next({ name: 'Home' }) : next()
    }
  },
  {
    path: '/register',
    name: 'Register',
    component: () => import(/* webpackChunkName: "login" */ '../views/register/Register.vue'),
    beforeEnter: (to, from, next) => {
      const { isLogin } = localStorage
      isLogin ? next({ name: 'Home' }) : next()
    }
  },
  {
    path: '/shop',
    name: 'Shop',
    component: () => import(/* webpackChunkName: "shop" */ '../views/shop/Shop')
  }
]

const router = createRouter({
  history: createWebHashHistory(),
  routes
})

router.beforeEach((to, from, next) => {
  const { isLogin } = localStorage
  // @ to.name 防止死循环
  const { name } = to
  const isLoginOrRegister = (name === 'Login' || name === 'Register');
  (isLogin || isLoginOrRegister) ? next() : next({ name: 'Login' })
})

export default router

访问页面的时候不一次性全部加载其他页面,增加访问速度

拆分NearbyList

import { ref } from '@vue/reactivity'
import { get } from '../../utils/request'
import ShopInfo from '../../components/ShopInfo'

const useNearbyListEffect = () => {
  const nearbyList = ref([])
  const getNearByList = async () => {
    const result = await get('/shop/hot-list')
    console.log(result)
    if (result?._error === 0 && result?.data?.length) { nearbyList.value = result.data }
  }
  return { nearbyList, getNearByList }
}

export default {
  name: 'Nearby',
  components: { ShopInfo },
  setup () {
    const { nearbyList, getNearByList } = useNearbyListEffect()

    getNearByList()

    return {
      nearbyList
    }
  }
}

组件拆分复用

拆分前

    <div class="nearby__item" v-for="item in nearbyList" :key="item.id">
      <img class="nearby__item__img" :src="item.imgSrc" />
      <div class="nearby__content">
        <div class="nearby__content__title">{{item.title}}</div>
        <div class="nearby__content__tags">
          <span class="nearby__content__tag">月售:{{item.sales}}</span>
          <span class="nearby__content__tag">起送:{{item.expressLimit}}</span>
          <span class="nearby__content__tag">基础运费:{{item.expressPrice}}</span>
         <span class="nearby__content__tag" v-for="(innerItem,index) in item.tags" :key="index">{{innerItem}}</span>
        </div>
        <p class="nearby__content__hightlight">{{item.desc}}</p>
      </div>
    </div>

拆分后

  <div class="nearby">
    <h3 class="nearby__title">附近店铺</h3>
    <ShopInfo
    v-for="item in nearbyList"
    :key="item.id"
    :item="item"
    ></ShopInfo>
  </div>

ShopInfo.vue

<template>
    <div class="shop">
      <img class="shop__img" :src="item.imgSrc" />
      <div :class="{'shop__content':true,'shop__content--bordered':hideBorder ? false:true}">
        <div class="shop__content__title">{{item.title}}</div>
        <div class="shop__content__tags">
          <span class="shop__content__tag">月售:{{item.sales}}</span>
          <span class="shop__content__tag">起送:{{item.expressLimit}}</span>
          <span class="shop__content__tag">基础运费:{{item.expressPrice}}</span>
        </div>
        <p class="shop__content__hightlight">{{item.desc}}</p>
      </div>
    </div>
</template>
<script>
export default {
  name: 'ShopInfo',
  props: ['item', 'hideBorder']
}
</script>
<style lang="scss" scoped>
  @import '../style/viriables.scss';
  @import '../style/mixins.scss';
   .shop {
      display: flex;
      //@ 小心margin重叠 margin-top:.12 rem;
      padding-top: .12rem;

      &__img {
        margin-right: .16rem;
        width: .56rem;
        height: .56rem;
      }
      &__content {
      padding-bottom: .12rem;
      &--bordered{
      border-bottom: 1px solid $content-bgColor;
      }
      flex: 1;
      &__title {
        line-height: .22rem;
        font-size: .16rem;
      }

      &__tags {
        margin-top: .08rem;
        line-height: .18rem;
        font-size: .13rem;
        color: $content-fontColor;
      }

      &__tag {
        margin-right: .16rem;
      }

      &__hightlight {
        margin: .08rem 0 0 0; //p标签自带 13px margin
        line-height: .18rem;
        font-size: .13rem;
        color: red;
      }
    }
    }
</style>

⭐ Flex : 1 到底是什么

https://zhuanlan.zhihu.com/p/136223806 https://blog.csdn.net/qq_40138556/article/details/103967529

制作Shop页面

搜索框和初步显示

Pasted image 20211126015617.png

<template>
<div class="wrapper">
  <div class="search">
    <div
    class="search__back iconfont"
    @click = "handleBackClick"
    >&#xe8ef;</div>
    <div class="search__content">
        <span class="search__content__icon iconfont">&#xe699;</span>
        <input class="search__content__input" placeholder="请输入商品名称"/>
    </div>
  </div>
  <ShopInfo
  :item="item"
  :hideBorder="true"
  />
</div>
</template>
<script>
import ShopInfo from '../../components/ShopInfo.vue'
import router from '../../router'
export default {
  name: 'Shop',
  components: {
    ShopInfo
  },
  setup () {
    const item = {
      id: 1,
      title: '沃尔玛',
      imgSrc: 'https://xiaonenglife.oss-cn-hangzhou.aliyuncs.com/projects/1121jingdong/f11.png',
      sales: 10000,
      expressLimit: 0,
      expressPrice: 5,
      desc: 'VIP 尊享满89元减4元运费卷(每月三张)'
    }
    const handleBackClick = () => {
      router.back()
    }
    return {
      item, handleBackClick
    }
  }
}
</script>
<style lang="scss" scoped>
.wrapper {
  padding: 0 .18rem;
}

.search{
  display:flex;
  margin: .2rem 0 .04rem 0;
  line-height: .38rem; // .32->.38
  &__back {
    width: .3rem;
    font-size: .21rem;
    color: #B6B6B6;
  }
  &__content{
    display: flex;
    flex:1;
    background: F5F5F5;
    border-radius: .16rem;
    &__icon{
      width: .44rem;
      font-size: .21rem;
      text-align: center;
      color: #B7B7B7;
    }
    &__input{
      display:block;
      width:100%;
      padding-right: .2rem;
      border:none;
      outline:none;
      background: none;
      height: .32rem;
      font-size: .14rem;
      color: #333;
      &::placeholder{
        color:#333
      }
    }
  }
}
</style>

下一步

2021.11.26