project from jirengu.com fangyinghang Frontend class
目录:
第一部分 MVC框架初步搭建
第二部分 引入类、继承优化代码
第三部分 MVC转向Vue
最小知识原则(基于模块化)的缺点:
页面一开始是空白的,没内容没样式,解决方法如下:
- index.html加菊花图 (
<img>),main.js加载完就删掉菊花(img.remove())- index.html加骨架图 (
<img>)- 用SSR技术(服务器端渲染技术)
模块功能是怎么使用的:
- 写index.html时,只需要写一个空的div,再引入一个main.js(
<script src="main.js"></script>)\- 在main.js里面,引入css(
import "./reset.css" 和import "./global.css") ; 依次引入app1.js一直到app4.js,不用关系这4个模块做什么
第一部分 MVC框架初步搭建
把上一章的html也与对应app合并在一起:
import './app1.css';
import $ from 'jquery';
const html =`
<section id="app1">
<div class="output">
<span id="number">100</span>
</div>
<div class="actions">
<button id="add1">+1</button>
<button id="minus1">-1</button>
<button id="mul2">*2</button>
<button id="divide2">÷2</button>
</div>
</section>
`
const $element = $(html).appendTo($('body>.page')) //把上述代码块添加回index.html里面
import './app2.css';
import $ from 'jquery';
const html =`
<section id="app2">
<ol class="tab-bar">
<li>1</li>
<li>2</li>
</ol>
<ol class="tab-content">
<li>内容1</li>
<li>内容2</li>
</ol>
</section>
`
const $element = $(html).appendTo($('body>.page')) //把上述代码块添加回index.html里面
import $ from 'jquery';
import './app3.css'
const html =`
<section id="app3">
<div class="square"></div>
</section>
`
const $element = $(html).appendTo($('body>.page')) //把上述代码块添加回index.html里面
import $ from 'jquery';
import './app4.css';
const html =`
<section id="app4">
<div class="circle"></div>
</section>
`
const $element = $(html).appendTo($('body>.page')) //把上述代码块添加回index.html里面
- MVC思想整理重复代码:把app1.js里面的内容,按照绑定事件和渲染数据的两个方向,按照MVC原则把所有跟数据相关的都放到M中,所有跟视图相关的都放到V中,其他的放在C中
import './app1.css';
import $ from 'jquery';
//初始化 html
const html=`
<section id="app1">
<div class="output">
<span id="number">100</span>
</div>
<div class="actions">
<button id="add1">+1</button>
<button id="minus1">-1</button>
<button id="mul2">*2</button>
<button id="divide2">÷2</button>
</div>
</section>
`
const $element = $(html).appendTo($('body>.page')) //把上述代码块添加回index.html里面
//寻找重要的元素
const $button1 = $('#add1');
const $button2 = $('#minus1');
const $button3 = $('#mul2');
const $button4 = $('#divide2');
const $number = $('#number');
//初始化数据
const n = localStorage.getItem('n')
//将数据渲染到页面
$number.text(n || 100)
//绑定鼠标事件
$button1.on('click',() => {
let n = parseInt($number.text());
n += 1;
localStorage.setItem('n',n)
$number.text(n);
});
$button2.on('click',() => {
let n = parseInt($number.text());
n -= 1;
localStorage.setItem('n',n)
$number.text(n);
});
$button3.on('click',() => {
let n = parseInt($number.text());
n *= 2;
localStorage.setItem('n',n)
$number.text(n);
});
$button4.on('click',() => {
let n = parseInt($number.text());
n /= 2;
localStorage.setItem('n',n)
$number.text(n);
});
- 先按MVC初步把代码整理成如下格式,但运行时,没有绑定事件的响应,
import './app1.css';
import $ from 'jquery';
//数据相关M
const m ={
//初始化数据
data: {
n: localStorage.getItem('n')
}
}
//视图相关V
const v ={
html:`
<section id="app1">
<div class="output">
<span id="number">100</span>
</div>
<div class="actions">
<button id="add1">+1</button>
<button id="minus1">-1</button>
<button id="mul2">*2</button>
<button id="divide2">÷2</button>
</div>
</section>
`,
render(){
const $element = $(v.html).appendTo($('body>.page')) //把上述代码块添加回index.html里面
},
update(){
//将数据渲染到页面
c.ui.number.text(m.data.n || 100)
},
}
//其他C(用户看不见的)
const c = {
ui: {
//寻找重要的元素,目的是绑定事件,为用户看不见的,放在C中
button1: $('#add1'),
button2: $('#minus1'),
button3: $('#mul2'),
button4: $('#divide2'),
number: $('#number')
},
//绑定鼠标事件
bindEvents(){
c.ui.button1.on('click',() => {
let n = parseInt(c.ui.number.text());
n += 1;
localStorage.setItem('n',n)
$number.text(n);
});
c.ui.button2.on('click',() => {
let n = parseInt(c.ui.number.text());
n -= 1;
localStorage.setItem('n',n)
$number.text(n);
});
c.ui.button3.on('click',() => {
let n = parseInt(c.ui.number.text());
n *= 2;
localStorage.setItem('n',n)
$number.text(n);
});
c.ui.button4.on('click',() => {
let n = parseInt(c.ui.number.text());
n /= 2;
localStorage.setItem('n',n)
$number.text(n);
})
}
}
//第一次渲染(初始化 html)
v.render()
- 解决绑定事件等其他问题
分析点击没有反应的原因:
console.log("$('#add1')")
debugger
console.log($('#add1'))
bindEvents(){
console.log('bindevents 执行了')
console.log(c.ui.button1)
- 控制台执行结果:
bindevents 执行了
app1.js:50 jQuery.fn.init {}
- 可能是button初始化在render之前,写一个初始化方法,不是一开始把ui声明好,而是在调用初始化init的时候,再去取ui的各个属性,这样可以保证此时取的是同步的。
const c = {
init(){
c.ui = {
//寻找重要的元素,目的是绑定事件,为用户看不见的,放在C中
button1: $('#add1'),
button2: $('#minus1'),
button3: $('#mul2'),
button4: $('#divide2'),
number: $('#number')
},
c.bindEvents()
v.render() //先渲染
c.init() //再初始化,然后绑定事件
- 1)接着把初始
<span>标签里的100,改为一个占位符,2)再把render()渲染时的内容与这个占位符关联,也就是把这个空占位符,替换为m.data.n,MVC中的数据M中的,3)绑定事件里的内容也对应改一下
const m ={
//初始化数据
data: {
n: localStorage.getItem('n')
}
}
<div class="output">
<span id="number">{{n}}</span> //1)
</div>
render(){
const $element = $(v.html.replace('{{n}}',m.data.n)) //2)
.appendTo($('body>.page')) //把上述代码块添加回index.html里面
},
bindEvents(){
c.ui.button1.on('click',() => {
let n = m.data.n
n += 1;
localStorage.setItem('n',n)
c.ui.number.text(n); //原来是$number,无法引用number
});
- 继续优化一下数据获取,删掉updata
//数据相关M
const m ={
data: {
//n: localStorage.getItem('n') 只能保存字符串
n: parseInt(localStorage.getItem('n'))
//由原来的字符串变成一个整数
}
}
const v ={
el:null,
render(){
if(v.el === null) {
v.el = $(v.html.replace('{{n}}',m.data.n))
.appendTo($('body>.page'))
}else{
const newEl = $(v.html.replace('{{n}}',m.data.n))
v.el.replaceWith(newEl)
v.el = newEl
}
},
- 以上代码初步实现点击页面的任何一点,就整个刷新自己,但是绑定事件失效了,因为之前是绑定的原button,而现在整个页面刷新后,已经变动了。
- 用最小知识原则,把render()放在初始化里面,让init接收一个参数,渲染到页面中的哪一块,在render()渲染时候,把container传给视图,与其相关的代码更新如下:
const c = {
init(container){
v.render(container)
render(container){
if(v.el === null) {
v.el = $(v.html.replace('{{n}}',m.data.n))
.appendTo($(container))
- 在初始化c的时候,要得到一个东西,而app1不知道外面有什么div,此时把
c.init()替换为export default c,app1需要别人传一个参数,才能运行
<body>
<div class="page">
<section id="app1"></section>
</div>
<script src="main.js"></script>
</body>
import x from "./app1.js";
x.init('#app1') //把页面中的app1传给这个模块,让其初始化
const v ={
el:null,
html:`
<div> //改动的
<div class="output">
<span id="number">{{n}}</span>
</div>
<div class="actions">
<button id="add1">+1</button>
<button id="minus1">-1</button>
<button id="mul2">*2</button>
<button id="divide2">÷2</button>
</div>
</div> //改动的
`,
- 首先做到这个container是不可变的,每次刷新时,section不变,点击事件时更整个里面的div,而绑定事件在section上,实现的代码如下:
import './app1.css';
import $ from 'jquery';
//数据相关M
const m ={
//初始化数据
data: {
//n: localStorage.getItem('n') 只能保存字符串
n: parseInt(localStorage.getItem('n'))
//由原来的字符串变成一个整数
}
}
//视图相关V
const v ={
el:null,
html:`
<div>
<div class="output">
<span id="number">{{n}}</span>
</div>
<div class="actions">
<button id="add1">+1</button>
<button id="minus1">-1</button>
<button id="mul2">*2</button>
<button id="divide2">÷2</button>
</div>
</div>
`,
init(container){
v.container = $(container);
v.render()
},
render(){
if(v.el === null) {
v.el = $(v.html.replace('{{n}}',m.data.n))
.appendTo($(v.container)) //存储container
}else{
const newEl = $(v.html.replace('{{n}}',m.data.n))
v.el.replaceWith(newEl)
v.el = newEl
}
},
}
//其他C(用户看不见的)
const c = {
init(container){
v.init(container)
c.bindEvents()
},
//绑定鼠标事件
bindEvents(){
v.container.on('click','#add1',()=>{
m.data.n += 1;
v.render()
}),
v.container.on('click','#minus1',()=>{
m.data.n -= 1;
v.render()
}),
v.container.on('click','#mul2',()=>{
m.data.n *= 2;
v.render()
}),
v.container.on('click','#divide2',()=>{
m.data.n /= 2;
v.render()
})
}
}
export default c
- 抽象为MVC经典结构,代码简化为:
import './app1.css';
import $ from 'jquery';
//数据相关M
const m ={
data: {}
}
//视图相关V
const v ={
el:null,
container:null,
html:`...`,
init(container){},
render(){}
}
//其他C(用户看不见的)
const c = {
init(container){},
bindEvents(){}
}
export default c
- 再次抽象为
view=render(data),视图即为渲染数据,
- 按照上图
视图即为渲染数据思路,代码更新如下:
import './app1.css';
import $ from 'jquery';
//数据相关M
const m = {
data: {
n:parseInt(localStorage.getItem('n'))
}
}
//视图相关V
const v ={
el:null, //el即为原来的container
html:`
<div>
<div class="output">
<span id="number">{{n}}</span>
</div>
<div class="actions">
<button id="add1">+1</button>
<button id="minus1">-1</button>
<button id="mul2">*2</button>
<button id="divide2">÷2</button>
</div>
</div>
`,
init(container){
v.el = $(container);
},
render(n){
if(v.el.children.length !== 0) v.el.empty()
$(v.html.replace('{{n}}', n))
.appendTo(v.el)
}
}
//其他C(用户看不见的)
const c = {
init(container){
v.init(container)
v.render(m.data.n) //第一次view = render(data)
c.bindEvents()
},
//绑定鼠标事件
bindEvents(){
v.el.on('click','#add1',()=>{
m.data.n += 1;
v.render(m.data.n)
}),
v.el.on('click','#minus1',()=>{
m.data.n -= 1;
v.render(m.data.n)
}),
v.el.on('click','#mul2',()=>{
m.data.n *= 2;
v.render(m.data.n)
}),
v.el.on('click','#divide2',()=>{
m.data.n /= 2;
v.render(m.data.n)
})
}
}
export default c
- 去掉重复的代码:
import './app1.css';
import $ from 'jquery';
//数据相关M
const m = {
data: {
n:parseInt(localStorage.getItem('n'))
}
}
//视图相关V
const v ={
el:null, //el即为原来的container
html:`
<div>
<div class="output">
<span id="number">{{n}}</span>
</div>
<div class="actions">
<button id="add1">+1</button>
<button id="minus1">-1</button>
<button id="mul2">*2</button>
<button id="divide2">÷2</button>
</div>
</div>
`,
init(container){
v.el = $(container);
},
render(n){
if(v.el.children.length !== 0) v.el.empty()
$(v.html.replace('{{n}}', n))
.appendTo(v.el)
}
}
//其他C(用户看不见的)
const c = {
init(container){
v.init(container)
v.render(m.data.n) //第一次view = render(data)
c.autoBindEvents()
},
events:{
'click #add1':'add',
'click #minus1':'minus',
'click #mul2':'mul',
'click #divide2':'div',
},
add(){
m.data.n += 1;
},
minus(){
m.data.n -= 1;
},
mul(){
m.data.n *= 2;
},
div(){
m.data.n /= 2;
},
autoBindEvents(){
for(let key in c.events){
const value = c[c.events[key]]
const spaceIndex = key.indexOf(' ')
const part1 = key.slice(0,spaceIndex)
const part2 = key.slice(spaceIndex + 1)
console.log(part1,',',part2)
v.el.on(part1,part2,value)
}
},
}
export default c
- 使用表驱动编程eventBus对象间通信:
import './app1.css';
import $ from 'jquery';
const eventBus = $({}) //引入一个空对象,主要是用它的on监听鼠标事件方法和trigger触发事件方法
console.log(eventBus);
//数据相关M
const m = {
data: {
n:parseInt(localStorage.getItem('n'))
},
create(){},
delete(){},
update(data){
Object.assign(m.data,data) //把data的所有属性赋值给m.data
eventBus.trigger('m:updated') //触发更新,别人去监听
localStorage.setItem('n',m.data.n) //每次刷新页面都保存更新后的值
},
get(){}
}
//视图相关V
const v ={
el:null, //el即为原来的container
html:`
<div>
<div class="output">
<span id="number">{{n}}</span>
</div>
<div class="actions">
<button id="add1">+1</button>
<button id="minus1">-1</button>
<button id="mul2">*2</button>
<button id="divide2">÷2</button>
</div>
</div>
`,
init(container){
v.el = $(container);
},
render(n){
if(v.el.children.length !== 0) v.el.empty()
$(v.html.replace('{{n}}', n))
.appendTo(v.el)
}
}
//其他C(用户看不见的)
const c = {
init(container){
v.init(container)
v.render(m.data.n) //第一次view = render(data)
c.autoBindEvents()
eventBus.on('m:updated',()=>{
v.render(m.data.n)
})
},
events:{
'click #add1':'add',
'click #minus1':'minus',
'click #mul2':'mul',
'click #divide2':'div',
},
add(){
m.update({n: m.data.n + 1})
},
minus(){
m.update({n: m.data.n - 1})
},
mul(){
m.update({n: m.data.n * 2})
},
div(){
m.update({n: m.data.n / 2})
},
autoBindEvents(){
for(let key in c.events){
const value = c[c.events[key]]
const spaceIndex = key.indexOf(' ')
const part1 = key.slice(0,spaceIndex)
const part2 = key.slice(spaceIndex + 1)
v.el.on(part1,part2,value)
}
},
}
export default c
以上代码完成对模块1的全部MVC优化
第二部分 引入类、继承优化代码
因为多次重构,代码已经乱套了,就不附代码了,把整个思考演进的过程做一个记录
-
基于事不过三的原则,有一个触发简化代码的点,那就是同样代码写3遍,可以抽象为一个函数;同样属性写3遍,抽象为共用属性(原型或类),同样原型写3遍,用继承。
-
本例app1和app2中,MVC的模块化以后,每个模块中还是发现有共用的属性,也就是可以抽象为共有属性,也就是原型
2.1 引入类:新建一个base文件夹,存放共用属性,分别建Model.js和View.js
class Model{
constructor(options){
this.data = options.data //data是在传参数时,赋值到对象本身上,而不是赋值到原型链里,下面的4个函数都是在原型里
}
create(){}
delete(){}
update(){}
get(){}
}
export default Model
import Model from './base/Model.js'
const m = new Model ({
data:{n:parseInt(localStorage.getItem('n'))}
})
m.update = (data)=>{
Object.assign(m.data, data)
eventBus.trigger('m:updated')
localStorage.setItem('n', m.data.n)
}
console.dir(m)得到的控制台信息为:\
- 上面的信息中,Model是m的构造函数,也就是其所属的类,本身有个data属性,n值为205,update是他自己的函数,原型中有
constructor、create、delete、get,即为类声明的五个方法。所以在调用update时,首先看自身有没有,若自身没有,就去原型调用
2.2 简化Model代码:
class Model {
constructor(options){
['data','update','create','delete','get'].forEach(key => {
if (key in options){
this[key] = options[key]
}
});
// this.data = options.data
// this.update = options.update
// this.create = options.create
// this.delete = options.delete
// this.get = options.get
}
}
export default Model
2.3 简化View代码:
import $ from 'jquery'
class View {
constructor({el,html,render}){
this.el = $(el)
this.html = $(html)
this.render = $(render)
}
export default View
2.4 发现View和Control有很多交叉的地方,说明v和c有非常重要的联系,所以考虑合并VC,也就是把v和c共用一个对象,让c就是v,v就是c。
2.5 合并以后的vc到底怎么命名呢?Vue.js框架认为:这是一个前端的库,主要处理的是视图,所以这个核心的库应该叫做View,重构一下所有c为v
2.6 新建EventBus的类,让M和V也继承EventBus,也就是让EventBus放成Model和View的父类,底层逻辑其实是EventBus就是EventTarget,所有的dom元素的类的类的类...倒数第2层都是EventTarget
import $ from 'jquery'
class EventBus {
constructor(){
this._eventBus = $(window)
}
on(eventName,fn){
return this._eventBus.on(eventName,fn)
}
trigger(eventName,data){
return this._eventBus.trigger(eventName,data)
}
off(eventName,fn){
return this._eventBus.off(eventName,data)
}
}
export default EventBus
2.7 引入继承:如何实现EventBus作为M和V的父类呢?如果你继承EventBus的类,那么必须在初始化constructor里面去调用父类的初始化,注意不加分号报错情况,
import EventBus from './EventBus'
class Model extends EventBus{
constructor(option) {
super() //调用 EventBus#constructor()
const keys = ['data','update','create','delete','get']
keys.forEach((key) => {
if (key in options){
this[key] = options[key]
}
}
}
第三部分 MVC转向Vue
- (MVC简化代码的极限就是Vue)
用Vue来写app1.js,终端中安装Vue,
yarn add vue模块用不同的技术,互相不影响。
- 因为Vue有两个版本,默认是不完整版,parcel如何引用Vue完整版,在
package.json添加如下代码,并重新运行parcel:
import Vue from 'vue'
'alias':{
"Vue" :"./node_modules/vue/dist/vue.common.js
}
- Vue 认为Model模块也没必要了,所以就有
new Vue({
el:el;
data:{n:parseFloat(localStorage.getItem('n'))}
})
- Vue 如何完成事件绑定,把运算的函数都放在
methods里面,下面代码用Vue写完以后,MVC和jquery就都没有用了,Vue就是MVC简化代码的极限了,如何保存数据到本地呢,用监听事件watch
const init = (el) => {
new Vue({
el: el;
data:{n:parseFloat(localStorage.getItem('n'))},
methods:{
add(){
this.n += 1 //Vue封装了this.data.n简化为this.n
},
minus(){
this.n -= 1
},
mul(){
this.n *= 2
},
div(){
this.n /= 2
},
}
watch:{
n:function(){ //本质是当n变化时,执行一个函数就把n存起来
localStorage.setItem('n', this.n) //用this就不能用剪头函数,因为箭头函数里面的this是window
}
}
template: ` //template就是以前的 html
<section>
<div class="output">
<span id="number">{{n}}</span>
</div>
<div class="actions">
<button @click="add">+1</button>
//原来的代码为:<button id="add1">+1</button>
<button @click="minus">-1</button>
<button @click="mul">*2</button>
<button @click="div">÷2</button>
</div>
</section>
`
})
}
第四部分 MVC总结
- MVC设计模式的思想,是从烂代码向框架式代码的过渡:
- 模块化实现最小知识原则:不同功能的实现分开写,各担其责,互不影响
- MVC应用于需求复杂的场景,具有普遍性,即代码都可以用M+V+C的思想来写,Vue局限只能满足最基础的需求
- 哈希表驱动编程
- 事不过三原则
- view = render() ,这是React的思维;在Vue中没有体现,而是监听了n的变化再保存到本地