哦哈哈

哦哈哈 查看完整档案

北京编辑北京工业大学  |  国际贸易 编辑教育行业  |   前端开发工程师 编辑填写个人主网站
编辑

我相信:发光不是太阳的权利,每个人都可以。
能给你带来帮助,我很开心❤️

个人动态

哦哈哈 发布了文章 · 11月21日

运行时的页面构建过程

前言

探索客户端Web应用程序的生命周期,从页面请求开始,到用户不同种类的交互,最后至页面被关闭。

生命周期

客户端web应用的生命周期,从用户在浏览器地址输入一串URL,或点击一个链接开始。
  • 输入URL
  • 浏览器:浏览器构建了发送至服务器的请求
  • 服务器:处理请求,并形成一个通常由HTML、CSS、JavaScript代码组成的响应。
  • 浏览器:接收到响应时,处理HTML、CSS、JavaScript并构建结果页面
  • 事件处理:监控事件队列,一次处理其中的一个事件
  • 事件等待:与页面元素交互,发生后调用事件处理器。
  • 应用的生命周期随着用户关掉或者离开页面而结束。

页面构建阶段

页面构建阶段的目标是建立Web应用的UI,其主要包括两个步骤:

  1. 解析HTML代码并构建文档对象模型 (DOM);
  2. 执行JavaScript代码。
步骤1会在浏览器处理HTML节点的过程中执行;
步骤二会在HTML解析到一种特殊节点——脚本节点(包含或引用JavaScript代码的节点)时执行。页面构建阶段中,这两个步骤会交替执行多次

HTML解析和DOM构建

页面构建阶段始于浏览器接收HTML代码时,该阶段为浏览器构建页面UI的基础。
通过解析收到的HTML代码,构建一个个HTML元素,构建DOM。在这种对HTML结构化表示的形式中,每个HTML元素都被当作一个节点。

在页面构建阶段,浏览器会遇到特殊类型的HTML元素——脚本元素,该元素用于包括JavaScript代码。每当解析到脚本元素时,浏览器就会停止从HTML构建DOM,并开始执行JavaScript代码。

执行JavaScript代码

所有包含在脚本元素中的JavaScript代码由浏览器的JavaScript引擎执行
由于代码的主要目的是提供动态页面,故而浏览器通过全局对象提供了一个API 使JavaScript引擎可以与之交互并改变页面内容。

JavaScript中的全局对象

浏览器暴露给JavaScript 引擎的主要全局对象是window对象,它代表了包含着一个页面的窗口。
window对象是获取所有其他全局对象、全局变量(甚至包含用户定义对象)和浏览器API的访问途径。
全局window对象最重要的属性是document,它代表了当前页面的DOM。通过使用这个对象,JavaScript代码就能在任何程度上改变DOM,包括修改或移除现存的节点,以及创建和插入新的节点。

事件处理器概览

浏览器执行环境的核心思想基于:同一时刻只能执行一个代码片段,即所谓的单线程执行模型。想象一下在银行柜台前排队,每个人进入一支队伍等待叫号并“处理”。但JavaScript则只开启了一个营业柜台!每当轮到某个顾客时(某个事件),只能处理该位顾客。
image

注册事件处理器

window.onload = function(){};
通过这种方式,事件处理器就会注册到load事件上(当DOM已经就绪并全部构建完成,就会触发这个事件)。
document.body.onclick = function(){};

把函数赋值给特殊属性是一种简单而直接的注册事件处理器方式。但是,我们并不推荐你使用这种方式来注册事件处理器,这是因为这种做法会带来缺点:对于某个事件只能注册一个事件处理器。也就是说,一不小心就会将上一个事件处理器改写掉。幸运的是,还有一种替代方案:addEventListener方法让我们能够注册尽可能多的事件,只要我们需要。

document.body.addEventListener("click", function() { 
    console.log(1)
});

document.body.addEventListener("click", function() { 
    console.log(2)
});

// 都会依次执行
查看原文

赞 0 收藏 0 评论 0

哦哈哈 关注了标签 · 11月20日

css

层叠样式表(英语:Cascading Style Sheets,简写CSS),又称串样式列表,由W3C定义和维护的标准,一种用来为结构化文档(如HTML文档或XML应用)添加样式(字体、间距和颜色等)的计算机语言。

关注 61560

哦哈哈 关注了标签 · 11月20日

webpack

Webpack 是一个前端资源加载/打包工具,只需要相对简单的配置就可以提供前端工程化需要的各种功能。

关注 3975

哦哈哈 发布了文章 · 11月12日

温故而知新篇之《JavaScript忍者秘籍(第二版)》学习总结(三)——闭包和作用域

前言

这本书的电子版我已经在学习总结第一篇已经放了下载链接了,可以去查看
温故而,知新篇之《JavaScript忍者秘籍(第二版)学习总结(一)——函数篇

你自律用在什么地方,什么地方就会成就你。要记住当你快顶不住的时候,磨难也快顶不住了。

加油吧,兄弟们


先来一个自增函数看看

var addnum= function(){
  var num=0; // 闭包内 参数私有化
  return  function(){
    return num++
  }
}
const Addnum= addnum()
Addnum()
console.log(Addnum()) // 1
console.log(Addnum()) // 2

封装私有变量

function Ninja() {
 var feints = 0;
 this.getFeints = function() {
  return feints;
  };
  this.feint = function() {
   feints++;
  };
}
var ninja1 = new Ninja();
ninja1.feint();

通过执行上下文来跟踪代码

具有两种类型的代码,那么就有两种执行上下文:全局执行上下文和函数执行上下文。二者最重要的差别是:

  • 全局执行上下文只有一个,当JavaScript程序开始执行时就已经创建了全局上下文;
  • 而函数执行上下文是在每次调用函数时,就会创建一个新的。

定义变量的关键字与词法环境

关键字var

var globalNinja = "Yoshi";  //⇽--- 使用关键字var定义全局变量

function reportActivity() {
  var functionActivity = "jumping";  //⇽--- 使用关键字var定义函数内部的局部变量

  for (var i = 1; i < 3; i++) {
     var forMessage = globalNinja + " " + functionActivity;   //⇽--- 使用关键字var在for循环中定义两个变量
     console.log(forMessage === "Yoshi jumping",
         "Yoshi is jumping within the for block");  //⇽--- 在for循环中可以访问块级变量,函数内的局部变量以及全局变量
     console.log(i, "Current loop counter:" + i);
  }

  console.log(i === 3 && forMessage === "Yoshi jumping",
      "Loop variables accessible outside of the loop");  //⇽--- 但是在for循环外部,仍然能访问for循环中定义的变量
  }

reportActivity();
console.log(typeof functionActivity === "undefined"
   && typeof i === "undefined" && typeof forMessage === "undefined",
   "We cannot see function variables outside of a function");  //⇽--- 函数外部无法访问函数内部的局部变量”

这源于通过var声明的变量实际上总是在距离最近的函数内或全局词法环境中注册的,不关注块级作用域。

使用let与const定义具有块级作用域的变量

const GLOBAL_NINJA = "Yoshi";  //⇽--- 使用const定义全局变量,全局静态变量通常用大写表示

function reportActivity() {
 const functionActivity = "jumping";   //⇽--- 使用const定义函数内的局部变量

  for (let i = 1; i < 3; i++) {
     let forMessage = GLOBAL_NINJA + " " + functionActivity;   //⇽--- 使用let在for循环中定义两个变量
     console.log(forMessage === "Yoshi jumping",
         "Yoshi is jumping within the for block");
     console.log(i, "Current loop counter:" + i);   //⇽--- 在for循环中,我们毫无意外地可以访问块级变量、函数变量和全局变量
  }

  console.log(typeof i === "undefined" && typeof forMessage === "undefined",
      "Loop variables not accessible outside the loop");  //⇽--- 现在,在for循环外部无法访问for循环内的变量
}

reportActivity();
console.log(typeof functionActivity === "undefined"
   && typeof i === "undefined" && typeof forMessage === "undefined",
   "We cannot see function variables outside of a function");  //⇽--- 自然地,在函数外部无法访问任何一个函数内部的变量

与var不同的是,let和const更加直接。let和const直接在最近的词法环境中定义变量(可以是在块级作用域内、循环内、函数内或全局环境内)。我们可以使用let和const定义块级别、函数级别、全局级别的变量。

在词法环境中注册标识符

const firstRonin = "Kiyokawa";
check(firstRonin);
function check(ronin) {
 assert(ronin === "Kiyokawa", "The ronin was checked! ");
}

先执行了check函数,后声明。却没有报错,因为什么呢?
JavaScript代码的执行事实上是分两个阶段进行的。

  • 在第一阶段,没有执行代码,但是JavaScript引擎会访问并注册在当前词法环境中所声明的变量和函数。JavaScript在第一阶段完成之后
  • 开始执行第二阶段,具体如何执行取决于变量的类型(let、var、const和函数声明)以及环境类型(全局环境、函数环境或块级作用域)。

JavaScript易用性的一个典型特征,是函数的声明顺序无关紧要。

函数重载

console.log(fun); // function
var fun =3;
function fun(){

}
console.log(fun); // 3

是函数变量提升了,但是我们需要通过词法环境对整个处理过程进行更深入的理解。
JavaScript的这种行为是由标识符注册的结果直接导致的。

  • 在处理过程的第2步中,通过函数声明进行定义的函数在代码执行之前对函数进行创建,并赋值给对应的标识符;
  • 在第3步,处理变量的声明,那些在当前环境中未声明的变量,将被赋值为undefined。
  • 示例中,在第2步——注册函数声明时,由于标识符fun已经存在,并未被赋值为undefined。这就是第1个测试fun是否是函数的断言执行通过的原因。之后,执行赋值语句var fun = 3,将数字3赋值给标识符fun。执行完这个赋值语句之后,fun就不再指向函数了,而是指向数字3。

小结

  • 通过闭包可以访问创建闭包时所处环境中的全部变量。闭包为函数创建时所处的作用域中的函数和变量,创建“安全气泡”。通过这种的方式,即使创建函数时所处的作用域已经消失,但是函数仍然能够获得执行时所需的全部内容。
  • 我们可以使用闭包的这些高级功能:
通过构造函数内的变量以及构造方法来模拟对象的私有属性。
处理回调函数,简化代码。
  • JavaScript引擎通过执行上下文栈(调用栈)跟踪函数的执行。每次调用函数时,都会创建新的函数执行上下文,并推入调用栈顶端。当函数执行完成后,对应的执行上下文将从调用栈中推出。
  • JavaScript引擎通过词法环境跟踪标识符(俗称作用域)。
  • 在JavaScript中,我们可以定义全局级别、函数级别甚至块级别的变量。
  • 可以使用关键字var、let与const定义变量:
关键字var定义距离最近的函数级变量或全局变量。
关键字let与const定义只能赋值一次的变量。
  • 闭包是JavaScript作用域规则的副作用。当函数创建时所在的作用域消失后,仍然能够调用函数。”

摘录来自: [美] John Resig Bear Bibeault Josip Maras. “JavaScript忍者秘籍(第2版)。” iBooks.

查看原文

赞 1 收藏 1 评论 0

哦哈哈 发布了文章 · 11月11日

温故而知新篇之《JavaScript忍者秘籍(第二版)》学习总结(二)——函数进阶篇

前言

这本书的电子版我已经在学习总结第一篇已经放了下载链接了,可以去查看
温故而,知新篇之《JavaScript忍者秘籍(第二版)学习总结(一)——函数篇

最近看了一句话,觉得挺不错的,分享下:

“不要把自己的努力看的太重,你自己觉得做这个很努力,做那个很努力;当你奔跑、跌倒的同时,有人比你已经先站起来跑了。
其实,努力应该是生活的常态,不应该被认为是稀缺、可贵的品质
而不是说 你努力了,你就应该和别人在很多事情上不一样。你应该不断的提升自己才是努力的意义。

我简直了,文邹邹的~~~哈哈哈哈。
来来来,说正题


arguments参数

arguments对象有一个名为length的属性,表示实参的确切个数。通过数组索引的方式可以获取单个参数的值,例如,arguments[2]将获取第三个参数

function(a,b,c){
//arguments[0]=a
//arguments[1]=b
console.log(arguments[2] === c) // true
}

你可能会被它的用法误导,毕竟它有length属性,而且可以通过数组下标的方式访问到每一个元素。但它并非JavaScript数组,如果你尝试在arguments对象上使用数组的方法(例如,上一章中用到的sort方法),会发现最终会报错。arguments对象仅是一个类数组的结构,在使用中要尤为注意。

arguments对象作为函数参数的别名

function infiltrate(person) {
 arguments[0] = 'ninja';
 person === 'ninja' // true
 
 //同理
 person = 'gardener';
 arguments[0] === 'gardener' // true
}

注意⚠️:

严格模式避免使用arguments别名

"use strict";  ⇽--- 开启严格模式
function infiltrate(person) {
    arguments[0] = 'ninja';  ⇽--- 改变第一个参数”
    arguments[0] === 'ninja' // true
    person === 'gardener' // false

}

this参数:函数上下文

函数调用

我们可以通过4种方式调用一个函数,每种方式之间有一些细微差别。
  • 作为一个函数(function)——skulk(),直接被调用。
  • 作为一个方法(method)——ninja.skulk(),关联在一个对象上,实现面向对象编程。
  • 作为一个构造函数(constructor)——new Ninja(),实例化一个新的对象。
  • 通过函数的apply或者call方法——skulk.apply(ninja)或者skulk.call(ninja)。

这里要补充下,书中有说,作为函数直接被调用、作为方法被调用
这段内容,我当时在理解的时候,以为自己理解了。后来做了几道题才明白这个意思。

var a=10
function b(){
  console.log(a) // 10 // 当前语义环境
  console.log(this.a) // 20  执时环境
}

var c={
  a:20,
  ninja:b
}
console.log(c.ninja())

注意⚠️:

上面的例子中,b不是c的一个方法,b是一个独立的函数。
当通过方法饮用去调用函数时,c.ninja(),对函数调用时,上下文就是c;

this,
当直接通过函数名调用,也就是将函数作为函数调用时,因为是在非严格模式下执行,因此预期的函数上下文结果应当是全局上下文(window)

作为构造函数调用

function Ninja() {
 this.skulk = function() {
   return this;
 };
}  ⇽--- 构造函数创建一个对象,并在该对象也就是函数上下文上添加一个属性skulk。这个skulk方法再次返回函数上下文,从而能让我们在函数外部检测函数上下文

var ninja1 = new Ninja();
var ninja2 = new Ninja();  ⇽--- 通过关键字new调用构造函数创建两个新对象,变量ninja1和变量ninja2分别引用了这两个新对象

console.log(ninja1.skulk() === ninja1,
 "The 1st ninja is skulking");
console.log(ninja2.skulk() === ninja2,
 "The 2nd ninja is skulking");  ⇽--- 检测已创建对象中的skulk方法。每个方法都应该返回自身已创建的对象 

当使用关键字new调用函数时,会创建一个空的对象实例并将其设置为构造函数的上下文(this参数)

注意⚠️:

面试的时候总是会问到。
一般来讲,当调用构造函数时会发生一系列特殊的操作
使用关键字new调用函数会触发以下几个动作。

1.创建一个新的空对象。
2.该对象作为this参数传递给构造函数,从而成为构造函数的函数上下文。
3.新构造的对象作为new运算符的返回值(除了我们很快要提到的情况之外)。
image

构造函数的返回值

  • 如果构造函数返回一个对象,则该对象将作为整个表达式的值返回,而传入构造函数的this将被丢弃。
  • 但是,如果构造函数返回的是非对象类型,则忽略返回值,返回新创建的对象。

箭头函数 this

var Button={
  clicked:false,
  click:()=>{
    this.clicked = true;
    console.log(this) // window
  }
}
Button.click()
function Button(){
  clicked=false
  click=()=>{
    this.clicked = true;
    console.log(this) // window
  }
}
Button()
click()

注意⚠️:

由于this值是在箭头函数创建时确定的
`在全局代码中定义对象字面量,在字面量中定义箭头函数,那么箭头函数内的this指向全局window对象`

小结:

  • 当调用函数时,除了传入在函数定义中显式声明的参数之外,同时还传入两个隐式参数:arguments与this
arguments参数是传入函数的所有参数的集合。具有length属性,表示传入参数的个数,通过arguments参数还可获取那些与函数形参不匹配的参数。在非严格模式下,arguments对象是函数参数的别名,修改arguments对象会修改函数实参,可以通过严格模式避免修改函数实参。
this表示函数上下文,即与函数调用相关联的对象。函数的定义方式和调用方式决定了this的取值。
  • 函数的调用方式有4种。
作为函数调用:skulk()。
作为方法调用:ninja.skulk()。
作为构造函数调用:new Ninja()。
通过apply与call方法调用:skulk.apply(ninja)或skulk.call(ninja)。
  • 函数的调用方式影响this的取值。
如果作为函数调用,在非严格模式下,this指向全局window对象;在严格模式下,this指向undefined。
作为方法调用,this通常指向调用的对象。
作为构造函数调用,this指向新创建的对象。
通过call或apply调用,this指向call或apply的第一个参数。
  • 箭头函数没有单独的this值,this在箭头函数创建时确定。
  • 所有函数均可使用bind方法,创建新函数,并绑定到bind方法传入的参数上。被绑定的函数与原始函数具有一致的行为。

摘录来自: [美] John Resig Bear Bibeault Josip Maras. “JavaScript忍者秘籍(第2版)。” iBooks.

查看原文

赞 0 收藏 0 评论 0

哦哈哈 发布了文章 · 11月9日

温故而知新篇之《JavaScript忍者秘籍(第二版)》学习总结(一)——函数篇

前言

这篇文章的总结和学习的内容,讲从第三章开始的;整本书学习时间大概是一星期的时间,利用零散的时间其实就可以学习完。
基础知识,是需要不断学习和补充的,如果总用不到的知识,我们就会忘记;加上我不是计算机专业的,所以也一直在努力💪弥补。

学习的方式

有句古话,“光说不做假把式”;这样就没意思了。
所以还是要敲代码和自己实践,这都是很重要的。

我挺推荐这个微信读书

因为你可以在闲暇时,用手机翻阅,还有读书朗诵的功能;有的时候,我们在看技术书的时候总是在走神。就可以用app的朗诵功能让你更加集中注意力。
image

一定要敲代码!!!!

一定要自己学会总结;这也很重要。好记性不如烂笔头,就是这个道理。

image

JavaScript忍者秘籍第二版的电子版

是epub的版本;《JavaScript忍者秘籍第二版.epub》;阅读电子版是为了更方便 我在电脑上去操作事例的时候更加容易,直接复制出来,打断点。
如果你需要,那么下面就是文档。
image

链接: https://pan.baidu.com/s/1L30_... 密码: n56j


下面就开始我们今天的总结和概况内容。我标题也说,这是重点内容总结,是方便你快速的记忆和回忆知识点,更好的方式还是系统的阅读一遍。

接下来的有些内容,我会直接引入文章的句子,请见怪不怪。
我会按照目录的顺序进行梳理

第3章 新手的第一堂函数课:定义与参数

对象能做的任何一件事,函数也都能做。

函数也是对象,唯一的特殊之处在于它是可调用的(invokable),即函数会被调用以便执行某项动作。

回调函数

function useless(ninjaCallback) {
 return ninjaCallback();
}
这个函数可能没什么用,但它反映了函数的一种能力,即将函数作为另一个函数的参数,随后通过参数来调用该函数。

存储函数

var store = {
 nextId: 1,  //⇽--- 跟踪下一个要被复制的函数
 cache: {},  //⇽--- 使用一个对象作为缓存,我们可以在其中存储函数
 add: function(fn) { 
   if (!fn.id) {
    fn.id = this.nextId++;
    this.cache[fn.id] = fn;
    return true;
  } 
 }  //⇽--- 仅当函数唯一时,将该函数加入缓存
};
function ninja(){}
assert(store.add(ninja), 
     "Function was safely added.");
assert(!store.add(ninja),
     "But it was only added once.");   //⇽--- 测试上面的代码按预期工作
fn.id将会是,传进来fn的唯一标示;如果在传进来相同的函数时,我们会发现这个id是存在的,就不会添加进来。
相当于修改了这个fn函数属性,添加了一个唯一标示ID;
如果是没有加进来的fn,那么就没有这个ID标示。
fn.id = this.nextId++;

函数定义

JavaScript提供了几种定义函数的方式,可以分为5类。

函数声明

 function myFun(){ return 1;}
每个函数声明以强制性的function开头,其后紧接着强制性的函数名,以及括号和括号内一列以逗号分隔的可选参数名。
函数声明必须具有函数名是因为它们是独立语句。一个函数的基本要求是它应该能够被调用,所以它必须具有一种被引用方式,于是唯一的方式就是通过它的名字

image

函数表达式

var myFun = function (){ return 1;}
JavaScript中的函数是第一类对象,除此以外也就意味着它们可以通过字面量创建,可以赋值给变量和属性,可以作为传递给其他函数的参数或函数的返回值。
注意⚠️:
函数声明和函数表达式除了在代码中的位置不同以外,还有一个`更重要的不同点`是:

* 函数声明来说,函数名是强制性的;
* 函数表达式来说,函数名则完全是可选的。
区分函数声明和函数表达式⚠️
《你不知道的JavaScript(上卷)》中,p27页,是这样写的
区分函数声明和函数表达式最简单的方法是看function关键字出现在声明的位置(不仅仅是一行代码,而是整个声明的位置)。 如果function是声明中第一个词,那么就是函数声明,否则就是函数表达式
立即函数

image
标准函数的调用和函数表达式的立即调用的对比

注意⚠️:加括号的函数表达式

函数表达式被包裹在一对括号内。为什么这样做呢?

其原因是纯语法层面的。JavaScript解析器必须能够轻易区分函数声明和函数表达式之间的区别
如果去掉包裹函数表达式的括号,把立即调用作为一个独立语句function() {}(3),JavaScript开始解析时便会结束,因为这个独立语句以function开头,那么解析器就会认为它在处理一个函数声明。每个函数声明必须有一个名字(然而这里并没有指定名字),所以程序执行到这里会报错为了避免错误,函数表达式要放在括号内,为JavaScript解析器指明它正在处理一个函数表达式而不是语句。
funciton(){33}

image

会直接报错,因为这个函数声明是错误的表现形式。

如果加上括号,或者是一元操作符
image

函数表达式要放在括号内,为JavaScript解析器指明它正在处理一个函数表达式而不是语句。
还有一种相对简单的替代方案(function(){}(33))也能达到相同目标(然而这种方案有些奇怪,故不常使用)。把立即函数的定义和调用都放在括号内,同样可以为JavaScript解析器指明它正在处理函数表达式。
image
不同于用加括号的方式区分函数表达式和函数声明,这里我们使用一元操作符+、-、!和~。这种做法也是用于向JavaScript引擎指明它处理的是表达式,而不是语句。

箭头函数

const myFun =()=>{ return 1;}
or 
var greet = name => "Greetings " + name;  //⇽--- 定义箭头函数”

image

注意这个新操作符——胖箭头符号=>(等号后面跟着大于号)是定义箭头函数的核心。

函数构造函数

new Function('a', 'b', 'return a + b')

生成器函数

function* myGen(){ yield 1; }

函数的实参和形参

  • 形参是我们定义函数时所列举的变量。
  • 实参是我们调用函数时所传递给函数的值。

image

函数形参是在函数定义时指定的,而且所有类型的函数都能有形参。

摘录来自: [美] John Resig Bear Bibeault Josip Maras. “JavaScript忍者秘籍(第2版)。” iBooks.

  • 函数声明(skulk函数的ninja形参)。
  • 函数表达式 (performAction函数的person和action形参)。
  • 箭头函数 (形参daimyo)。

从另一方面说,实参则与函数的调用相联系。它们是函数调用时所传给函数的值。

  • 字符串Hattori以函数实参的形式传递给函数skulk。
  • 字符串Oda Nobunaga以函数实参的形式传递给函数rule。
  • skulk函数的形参ninja作为实参传递给函数performAction。
可以这样理解哈:
函数声明和函数表达式,箭头函数,在声明的时候在括号里面的是形参,
函数调用时,传递给函数的是实参

剩余参数

function multiMax(first, ...remainingNumbers) {  ⇽--- 剩余参数以…作前缀
 var sorted = remainingNumbers.sort(function(a, b) {
   return b – a;   ⇽--- 以降序排序余下参数
 });
 return first * sorted[0];
}
assert(multiMax(3, 1, 2, 3) == 9,   ⇽--- 函数调用方式和其他函数类似
    "3*3=9 (First arg, by largest.)")

为函数的最后一个命名参数前加上省略号(...)前缀,这个参数就变成了一个叫作剩余参数的数组,数组内包含着传入的剩余的参数。

注意⚠️:只有函数的最后一个参数才能是剩余参数。

ES6中处理默认参数的方式

function performAction(ninja, action = "skulking"){   ⇽---  ES6中可以为函数的形参赋值
  return ninja + " " + action;
}

assert(performAction("Fuma") === "Fuma skulking",
    "The default value is used for Fuma");

assert(performAction("Yoshi") === "Yoshi skulking",
    "The default value is used for Yoshi");

assert(performAction("Hattori") === "Hattori skulking",
    "The default value is used for Hattori");  ⇽--- 若没传入值,则使用默认参数

assert(performAction("Yagyu", "sneaking") === "Yagyu sneaking",
    "Yagyu can do whatever he pleases, even sneak!");   ⇽--- 使用了传入的参数

小结

  • 把JavaScript看作函数式语言你就能书写复杂代码。
  • 作为第一类对象,函数和JavaScript中其他对象一样。类似于其他对象类型,函数具有以下功能。
  • 通过字面量创建。
  • 赋值给变量或属性。
  • 作为函数参数传递。
  • 作为函数的结果返回。
  • 赋值给属性和方法。
  • 回调函数是被代码随后“回来调用”的函数,它是一种很常用的函数,特别是在事件处理场景下。
  • 函数具有属性,而且这些属性能够被存储任何信息,我们可以利用这个特性来做很多事情;例如:
  • 可以在函数属性中存储另一个函数用于之后的引用和调用。
  • 可以用函数属性创建一个缓存(记忆),用于减少不必要的计算。
  • 有很多不同类型的函数:函数声明、函数表达式、箭头函数以及函数生成器等。
  • 函数声明和函数表达式是两种最主要的函数类型。函数声明必须具有函数名,在代码中它也必须作为一个独立的语句存在。函数表达式可以不必有函数名,但此时它就必须作为其他语句的一部分。
  • 箭头函数是JavaScript的一个新增特性,“这个特性让我们可以使用更简洁的方式来定义函数。
  • 形参是函数定义时列出的变量,而实参是函数调用时传递给函数的值。
  • 函数的形参列表和实参列表长度可以不同。
  • 未赋值的形参求值得到undefined。
  • 传入的额外实参不会被赋给任何一个命名形参。
  • 剩余参数和默认参数是JavaScript的新特性。
  • 剩余参数——不与任何形参名相匹配的额外实参可以通过剩余参数来引用。
  • 默认参数——函数调用时,若没传入参数,默认参数可以给函数提供缺省的参数值。
摘录来自: [美] John Resig Bear Bibeault Josip Maras. “JavaScript忍者秘籍(第2版)。” iBooks.
查看原文

赞 0 收藏 0 评论 0

哦哈哈 发布了文章 · 9月14日

MVC、MVP和MVVM

前言

在web1.0时代,并没有前端的概念,要写就就后端一起写了。前后端的代码杂糅到一起,比如php开发前后端,随后衍生出MVC开发模式和框架。

web1.0时代

起初的MVC

目标

数据、视图、以及业务逻辑控制分层;这样就可以把代码切割成功能独立的模块。

优点

使用了这种分层架构,实则清晰,代码易维护。

实现了一定程度的前后端的分离,但是还不是很清晰

起初的MVC仅限于服务端(后端),在服务器端渲染。前端只完成了后端开发中的view层(只是写模板文件,按照模版语法去写动态内容);
Model层提供数据查询。
View层通过模板引擎来进行解析。解析成真正的HTML内容,浏览器去渲染
缺点
  1. 前端页面开发效率不高
前端之前都是以静态页面出现的;很少的js交互;将代码交给后端程序员,后端在用模板语法对它进行动态化的改造,效率不高。
  1. 前后端指责不清晰
一般都是一个程序员,前后端一起抓,要会的也很多,前端的兼容性,后端的语法等等。

web2.0时代

谷歌的Gmai的出现,ajax技术开始风靡全球,从而前后端的职责更加清晰了。前端可以通过Ajax与后端进行数据交互

前端: 使用HTML、CSS、 Javascript(在Js中来撰写Ajax的请求),前端通过Ajax请求来获取数据,前端在进行交互和渲染。
后端:只需要把数据的基本结构返回给前端。

通过Ajax与后端服务器进行数据交互;前端只需要开发页面内容,数据由后端提供;ajax可以使页面实现部分刷新,减少服务端负载和流量消耗。这是才有了专职的前端来法工程师,同时前端的各种类库就慢慢发展起来,比如早前的JQuery

优点

流量消耗变小了,局部刷新;用户体验提升

缺点

开发模式承载了更复杂的业务需求,一旦应用规模增大,还是会导致难以维护,因为Html、Css、 JS还是杂糅在一起。从而我们需要设计模式和框架。
前端的MVC才会随之而来

MVC

MVC框架分为,前端MVC和后端MVC;
前端的MVC与后端的相似,模拟这后端的架构;都具有View、Controller和Model
Model: 模型负责保存应用数据,与后台数据进行同步
Controller:控制器负责业务逻辑,根据用户行为对Model数据进行修改
View: 负责视图的展示,将model中的数据可视化出来

来个模型瞅瞅~~
真够难画的,一图弄了10分钟。也没有多好看,应该是我不太用这个软件的问题,如果有错,那一定是我的错。哈哈哈哈哈
image

视图会通过事件去通知控制器,控制器去改模型,模型在尝试用某种办法通知视图去更新。
理论上可行,但是往往在实际开发中,并不会这样操作。因为开发过程并不灵活。比如: 一个小的事件操作,都必须经过这样的一个流程,那么开发就不便捷。

在实际中场景中,可能是另一种模式,这样的:
来~~~画图,算了用网上找的把。
image
这个模式看上去是方便一些,backbone.js框架就是这种模式;
View可以操作Model,Model改变也可以View;从而控制器层就显得很单薄,可有可无;(控制器里面就变成了简单的数据坚监听和调用)
缺点: 错误很难定位,数据混乱

`因为MVC框架出现的缺陷,从而有了MVVM框架。最早是AngularJS 用的MVVM框架模式。
MVP模式前端开发并不常见,但是在安卓原生开发中,开发者还是会考虑到它。`

MVP

MVP和MVC很接近,刚才看到了,可能会出现混乱的情况,MVP就 做了一个中间人,Presenter: 负责View和Model之间的数据流动,如果数据和视图非要交互就必须要通过中间人。
image

Presenter负责和Model进行双向交互,还有和View进行双向交互。
如果业务复杂一点,Presenter的体积增大、臃肿,就很难维护;

MVVM

MVVM可以分解成(Model-View-ViewModel);ViewModel可以理解为在Presenter基础上的进阶。
image
ViewModel,主要是胶水层,核心思想是简化
1.数据发生变化,如何知道变化,通过数据响应式的机制,用某种机制知道这个数据的变化,自动的去响应数据的变化,自动去做更新;内部知道了数据的变化,不需要用户来操作。

  1. 更新,以前做Dom操作,用JQ;数据变了要做Dom操作来更新(代码多,效率不高);所以有虚拟Dom的方式去做更新,根据精准的diff算法来做比对;达到高效的结果。

`通过响应式和虚拟Dom来做到数据驱动。
如果数据变,直接视图更新;开发人员只需要修改数据,不用操作Dom;不需要操心其他事情。`

视图层如何改变数据层?
和之前一样,还是做数据监听,模版引擎会有事件处理的语法,通过这些语法,轻易的写出这些事件监听的方式

优点

ViewModel 通过实现一套数据响应式机制自动响应Model中数据变化
ViewModel 也有一套更新策略自动将数据转化为视图更新
通过事件监听响应View中用户交互修改Model中数据
从而使开发者专注于业务逻辑,兼顾开发效率和可维护性

总结

三个框架,反映了web前端发展的进程,职责都是:

  1. 代码分层
  2. 职责(前后端)划分
  3. 解决维护性问题
  4. 解决 Model和View的耦合问题

MVC 早起专注应用在后端;前端早起的应用BackBone.js.

优点:分层清晰(刚开始,算是比之前清晰了)
缺点: 数据流混乱,灵活性带来的维护性问题

MVP 模式是由MVC的进化形式,Persenter作为中间层负责MV通信,解决了两者耦合关系,但是中间层过于臃肿会导致维护问题

MVVM模式在前端领域目前更广泛。不仅解决了MV的耦合问题,解决了维护两者映射关系的大量繁杂代码和Dom操作代码;提高了开发效率。

查看原文

赞 0 收藏 0 评论 0

哦哈哈 发布了文章 · 9月9日

初识webpack4.0-基础原理

前言

之前都是使用,没有系统的完整的好好学习过一遍webpack的原理,趁现在工作有些闲空,整理下。加强下记忆

什么是webpack?

不想放webpack官网那张图,感觉都快看吐了。
算了还是妥协把,要不然不好说清楚.....
image
官网的哈,看能看出的来,那继续了~~

分为三个部分

左侧:

写项目的源代码,源文件
左侧有一个入口,最上面的.js
一个一个的方格可以理解成一个一个的模块,因为webpack是基于node的,所以可以用模块的概念来区分文件。
箭头的表示➡️一个模块可以依赖多个模块
箭头➡️表示他们之间的依赖关系
左侧会有很多,后缀的文件是浏览器无法识别的。例如:.sass .hbs .cjs
所以需要经过webpack 来构建一次打包

经过webpack的构建

右侧:

输出浏览器可以正确执行的文件,资源

我们还可以利用webpack来压缩 js 、压缩图片 。
所以webpack就是一个模块打包器

webpack安装

分为两种安装方式

  • 项目安装(推荐)
    有些项目可能很老,依赖的webpack的版本比较低。
    最新的是webpack4.X; webpack5.x还没有发布
  • 全局安装
    如果是全局安装,版本会固定化。如果是去修改来的版本会冲突,依赖冲突等。所以根据项目来安装webpack,会好些。

安装:
webpacl4.x之后,这两个必须要都安装,webpack和 webpack-cli;不建议分开装。

创建项目之后,现需要npm init一下,之后在安装。初始化一下。这个大家应该都知道。

npm install webpack webpack-cli -D

webpack启动

webpack4.0发布的时候,有说,支持零配置启动。结果发现,哈哈哈哈哈哈哈哈~~~没啥用。基本用不到项目中。

我们来看下零配置:
安装好webpack之后,我们来创建一个入口文件,index.js

零配置 :

默认入口:./src/index.js
如果没有src 下面的index.js 打包是不成功的。
默认出口:输出的目录是 ./dist,输出的资源名称:main.js

image
接下来,启动webpack,启动方式有两种。

第一种启动方式:

npx webpack

为啥用npx启动项目

npm5.2版本,就自带npx这个功能;
现在我们启动的整个项目webpack是局部安装的
如果全局没有安装webpack,全局执行 webpack 就会报错,command not found。它会去全局环境变量中去寻找。
通过npx 启动webpack,它会在当前项目下的node_modules/.bin 目录去找它的软连接,在.bin 目录下找到webpack,去启动它。

image

来看下,打包好里面都显示了啥?

  • 构建的哈希版本
Hash: 096bb4bcb72f81514554
  • 执行构建的webpack版本
Version: webpack 4.44.1
  • 执行构建的时长
Time: 217ms
  • 构造的内容是什么?

// 输出的资源叫main.js
// 这个资源是958 bytes
// Chunks 是为 0

Built at: 2020-08-25 14:45:08
  Asset       Size  Chunks             Chunk Names
main.js  958 bytes       0  [emitted]  main

// 这块表示 输出的main.js,是根据./src/index.js文件built而来的
Entrypoint main = main.js
[0] ./src/index.js 29 bytes {0} [built]
  • 警告内容

自己百度吧,好吗? 用我小学的英语水平大概就是:环境没有设置:就是生产环境和开发环境没设置。会走一个默认的值,默认值是production(生产模式);

生产模式下的代码是进行压缩的,现在可以看下,main.js
image
WARNING in configuration
The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/configuration/mode/

测试打包好的main.js;是否可以使用
在dist里面,手动创建一个index.html,然后手动引入main.js;
dist/index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
  <script data-original="./main.js"></script>
</body>
</html>

第二种启动方式:

在package.json,的scripts中,添加启动命令

  "scripts": {
    "dev": "webpack"
  },

之后执行

npm run dev

效果和上面是一样的,就不进行在进行演示了。

当我们在scripts中去定定义了执行命令dev,执行webpack;是从本地的node_modules/.bin目录的软连接去找webpack

webpack配置文件,自定义配置文件

在根目录下创建,webpack.config.js
webpack.config.js 就是webpack的自定义配置文件
当我们执行 npx webpack
首先会去项目下去查找 是否有webpack.config.js,这个配置文件;如果没有会走默认配置;如果存在,就会按照webpack.config.js配置里面的内容去进行构建

webpack.config.js

webpack是基于node 的,所以可以直接导出模块
module.exports={
    entry(入口):(有三种类型)
    output(出口): {
        // 指定输入资源存放目录,位置
        // path 必须是绝对路径
        path:,
        //输出文件名
        filename:,
    }
    // 刚刚报的警告,就是这个值没有设置。
    mode: 构建模式
}

webpack.config.js

const path =require('path')
module.exports={
  entry:'./src/index.js',
  output:{
    path:path.resolve(__dirname,'./build'),
    filename:'main.js'
  },
  // 开发模式
  mode:'development'
}
npx webpack

image

目录新创建了一个build;main.js中的文件没有被压缩,开发模式下,文件不会被打包。

image

main.js 文件内容分析

main中是一个自执行的闭包函数
//行参 modules 所有的模块
(funcition(){modules})()
image

等下,我在创建个依赖js。为了方便看这个modules里面的内容。
image

image

modules是什么?

//modules 是一个对象,这个对象记录了,从入口模块出发,以及依赖模块的信息

image

来看下,在src/index.js中的内容

/***/ "./src/index.js":
/*!**********************!*\
  !*** ./src/index.js ***!
  \**********************/
/*! no exports provided */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _a_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./a.js */ \"./src/a.js\");\n\nconsole.log('hello webpack'+_a_js__WEBPACK_IMPORTED_MODULE_0__[\"str\"]);\n\n//# sourceURL=webpack:///./src/index.js?");

/***/ })
引入a.js的方式改为了__webpack_require_
通过webpack构建,对内容进行了一层处理,模块化语句也进行了一次处理
__webpack_require_ 在自执行函数中定义了这个方法的执行。

(先简单说明下,后面会写具体的文档)


继续webpack配置文件;比如我们在Vue cli工具创建的文件;所以我们可以自己去定义webpack配置文件的名称,可以不叫webpack.config.js;默认是走这个文件;
比如在根目录下新建一个,webpack.ohh.js;那如何让webpack走这个配置文件呢,
需要在package.js

// --config 用这个--config来进行匹配 
"scripts":{
    "dev": "webpack --config ./webpack.ohh.js"
}

image
执行npm run dev 进行打包

webpack核心概念

  • enrty:webpack执行构建的入口,默认是./src/index.js;
看官网那个图,就能看出来,需要一个js文件入口。当然这个文件入口也是可以修改的。
创建一个webpack.config.js,entry 就可以进行修改。
  • output: 配置资源输出的位置和名称,
打包好的文件放在什么位置,叫什么名字
  • mode:打包构建模式,有开发模式和生产模式两种。
development:开发模式,不压缩代码,有利于查找错误
productiont:生产模式,发布到线上的代码,代码会被压缩
  • chunk:代码片段,由入口模块文件与依赖模块的内容生成的。
  • module:模块,webpack基于nodeJs,有一切皆模块的概念
在刚刚我们打包的 a.js or index.js ;或者后面会去添加的 index.css,图片等,都可以成为模块

bundle: 打包输出到资源目录的文件,构建成功后的输出文件

顺一顺~~~~
bundle chunk module,三者的关系:

SPA 项目 一个单页面应用,只会有一个bundle 文件;
MPA 项目 多页面应用,就会有多页面的bundle文件;

比如index.js 和login.js

上面放了很多都是跑的SPA 单页面的应用;来看一个多页面MPA的。
首先是配置:

webpack.ohh.js

const path = require('path')
// 多入口
module.exports={
  entry:{
    index:'./src/index.js',
    login:'./src/login.js'
  },
  output:{
    path:path.resolve(__dirname, 'ohh'),
    filename:'[name].js'
  },
  mode:'development'
}

image

一个bundle 文件,对应一个chunk里面会有多个片段

MPA 多页面应用,才会存在多个chunks;
SPA 单页面应用,只有一个入口,一个chunk,(但是可能会有多个片段组成:

比如index.js中引入了a.js;那么chunk就有两个片段:一个是index、一个是a文件;);

**一个chunk对应一个entry
一个entry对应多个module**

  • loader:模块转换;webpack默认只支持js模块,json模块。

css模块,和图片模块都不支持。就需要用到loader,
比如:

src/index.css

body{
 background: pink;
} 

src/index.js
引入 css

// a.js
// export const str='这是ajs';
import {str} from './a.js'
import css from './index.css';
console.log('hello webpack'+str);

需要去配置loader
在配置文件,当然你也可以额写在webpack.config.js中。
创建一个module,写入加载规则
webpack.ohh.js

const path = require('path')
 // 多入口
module.exports={
 entry:{
   index:'./src/index.js',
   login:'./src/login.js'
 },
 output:{ path:path.resolve(__dirname, 'ohh'),
   filename:'[name].js'
 },
 mode:'development',
 module:{ // 模块
   rules:[ // 规则
     {
       test: /\.css$/,
       use: "css-loader"
     },
   ]
 }
}

进行安装 css-loader

npm install css-loader -D

之后来跑一下项目:

npm run dev

看一下打包结果
image
没有报错 也没有显示,因为:
css-loader 的作用非常的单一,只是让我们的webpack知道了,如何去处理css的语法,如何让css语法以字符串的方式,放到bundle文件里面去。
image

否则就会报错,接下来要做的是,将这段代码提取出来放到html中。

  npm install style-loader -D

style-loader 的作用:
将css提取出来,并且在html的头部,动态的生成style标签,将css放到里面去。
这时,还需要去修改下配置文件的module
webpack的配置文件修改;

module:{
  rules:[
      {
          test:/\.css$/,
          use:["style-loader", "css-loader"]
      }
  ]
 }
添加一个style-loader
loader的自行顺序,user:[];执行顺序是从后往前的,所以先要把css解析出来,以后再用style-loader,生成style标签,将css内容放进去。【从右到左】
  • 安装完了style-loader
  • 配置文件修改了css文件的匹配规则
  • 执行 npm run dev,查看页面加载情况
  • 在打包好的文件中,【自己 创建】创建一个index.html;这样可以看到效果
image
浏览器打开这个index.html
image
展示成功~~~~~!耶✌️
  • 结论:
    css-loader和style-loader需要配合来使用
  • 为什么css-loader功能单一,为什么要配合style-loader 才生效?

因为 自定义loader规定,一个loader只做一件事情。
style-loader的功能也很单一,把样式放在一个动态生成的style标签里面。所以不会在本地打包的html文件中显示出来,是通过js动态生成的。所以引入js就可以。
如果像将css处理成单独的文件,而不是在页面创建一个style标签的形式放在html里面。那就不能用style-loader;
index.less的文件,我们就有需要去加新的匹配规则,["style-loader","css-loader","less-loader"]

  • plugin:webpack 的一个功能扩展

举例: 像我们刚刚手动去创建的index.html
如果我们想要看到效果就需要,在打包我的文件夹中去手动创建index.html;非常的繁琐,那么如何解决呢?这个时候利用plugin;相当于webpack 的一个功能扩展。【官网中-右上角导航就有plugin,找一个合适的进行使用】
接下来,以如何自动创建index.html 来进行开展plugin的使用讨论

先安装:

 npm install html-webpack-plugin -D

image

  • 配置webpack默认项:

webpack.config.js

const path =require('path')
// 这里导入html-webpack-plugin
const htmlWebpackPlugin = require('html-webpack-plugin')
// 单入口
module.exports={
    entry:'./src/index.js',
    output:{
        path:path.resolve(__dirname,'./build'),
        filename:'main.js'
    },
    // 开发模式
    mode:'development',
    module:{
        rules:[
            {
                test:/.css$/,
                use:['style-loader','css-loader']
            }
        ]
    },
    plugins:[
        new htmlWebpackPlugin({ // plugin的使用,先new一下
            template:'./src/index.html', // 模板文件
            filename:'index.html' //倒出的文件名
        })
    ]
}

因为是在默认文件配置,所以跑起来,需要执行

 npx webpack

效果如下:
image
这样就打包成功了,那么直接用浏览器打开index.html
就可以看到效果了。并不在截图,因为和上面那个图片展示是一样的。就换成了自动生成html而已,仅此而已。
继续~~~~~(为什么要总结原理,我越写饿了,难过)

为了保证每次创建出来的build,打包文件都是全新的,而且不存在冗余的文件,比如说,当我们修改了一下 plugins中的
new htmlWebpackPlugin中的打包目录地址的配置:
filename:'html/ohh.html'
那么上次打包在根目录下的html并不会删除,就冗余了呗。
我们就需要用到plugin来进行配置
先安装:

npm install clean-webpack-plugin -D

使用,都是一样的

const {CleanWebpackPlugin} = require('clean-webpack-plugin')
// 在pulugins中new一
plugins:[
   new htmlWebpackPlugin({ // plugin的使用,先new一下
   template:'./src/index.html', // 模板文件
   filename:'index.html' //倒出的文件名
   }),
    new CleanWebpackPlugin()
]
  • 引入css文件而不是写在style标签里面,把样式抽离成独立的文件

首先安装plugin

npm install mini-css-extract-plugin -D

修改webpack的配置文件

  1. 引入plugin
plugins:[
   new miniCssExtractPlugin({
       filename:'css/index.css'
   })
]
  1. 修改css文件的模块匹配规则和渲染方式
module:{
   rules:[
       {
           test:/.css$/,
           use:[miniCssExtractPlugin.loader,'css-loader']
       }
   ]
},

image

基本原理就差不多顺明白了。总结下


总结

webpack.config.js: webpack配置文件

基本概念

  • entry:webpack执行构建任务的入口文件
  • output:执行构建后,资源配置的输出位置和名称
  • module: 模块
  • chunk:入口文件和依赖模块组合,经过webpack构建成生的一段代码片段
  • bundle: webpack构建完成够后输出的文件
  • loader: 模块转化,让webpack可以支持转化更多的模块类型
  • plugin:插件,让webpack功能更强大
  • modle: 打包构建模式,开发模式or生产模式

核心配置

  • enrty:String|[]|{}
[] : 单页面应用
{}: 可以SPA or MPA。多入口对应多出口文件
  • output:{path:,filename:“”}
path:必须是绝对路径
filename 可以使用占位符 [name] [hash]等
  • mode:String
参数:“production” “development” “node”
  • module :{}
{rules:[]}
  • plugins : []
    • *

webpack的学习,其实就是各种plugin和module的使用,用多了就写的顺了,所以主要的还是基础概念要清楚,所以才单独拿出来进行一个梳理,鄙人见解哈。

查看原文

赞 0 收藏 0 评论 0

哦哈哈 发布了文章 · 8月19日

node-创建vue-cli工具

搭建文件

mkdir vue-auto-router-cli
cd vue-auto-router-cli
npm init -y
npm i commander download-git-repo ora handlebars figlet clear chalk open -s

文件目录

vue-auto-router-cli
├──bin #程序执行入口
├────mycli.js #整个程序入口文件
├──node_modules 
├──package-lock.json 
├──package.json

mycli.js

#!/usr/bin/env node 
// 以sheel 脚本的形式来执行
// 执行解释器类型是node。使用node去执行
// 程序执行入口bin
// 整个程序入口文件mycli

console.log('cli.....')

修改package.json

  "bin":{
    "mycli" : "./bin/mycli.js"
  },
npm link

在项目目录下执行,相当于把入口文件链接到全局
当在其他文件下执行,mycli 这个指令,都会打印出来./bin/mycli.js文件里面的内容

mycli.js

// 定制命令行界面的介绍
// 比如我们执行 vue cli 的时候,出来的命令行界面

const program = require('commander')
// -v 时候 的版本号。我们直接用package上的version字段
program.version(require('../package.json').version)

// 创建,命令,命令行定制 
// <name> 代表参数
// 策略模式开发
program
    .command('init <name>')
    .description('init project') //描写
    .action(name=>{ // 做的事情
      console.log('init'+ name)
      }
    )
  program.parse(process.argv)// 现在进程的argv,最后一行是固定的

WX20200727-161525@2x.png

打印process.argv的结果
process.argv [ '/Users/chensi/nvm/versions/node/v8.16.0/bin/node',
  '/Users/chensi/nvm/versions/node/v8.16.0/bin/mycli',
  'init',
  'abc' ]

实现init函数的功能

下载代码
下载依赖

目录结构
根目录下:

├──lib 
├────download.js 
├────init.js 

lib/download.js

`const {promisify} =require('util')`
// 实现一个clone函数,远程去拉取
// 是一个异步函数
module.exports.clone = async function (repo,desc) {
  // repo 从哪拉
  // desc 拉到哪里去
  const download =  promisify(require('download-git-repo'))

  //进度条
  const ora = require('ora')
  // 进度条内容
  const process = ora(`下载....${repo}`)
  // 开启进度条
  process.start()
  // 开始下载
  await download(repo,desc)
  //显示完成
  process.succeed()
}

lib/init.js

// 打印欢迎界面
const {clone} =require('./download')
const {promisify} = require('util')

const figlet = promisify(require('figlet'))
const clear = require('clear')
const chalk = require('chalk')

const log=content=>console.log(chalk.green(content))
module.exports = async name=>{
  //清屏幕
  clear()
  // 打印
  const data= await figlet('mycli-ohh Welocome')
  log(data)
}

bin/mycli.js

修改命令的执行结果
 .action(require('../lib/init')

WX20200727-171929@2x.png

下载代码

bin/mycli.js

下载vue框架
module.exports = async name=>{
  //清屏幕
  clear()
  // 打印
  const data= await figlet('mycli-ohh Welocome')
  log(data)
  // 克隆
  log('🚀创建项目:'+name) //  name是传进来的参数,创建的项目名称
  // 当我们去执行 myclie init abc 时,相当于把项目克隆下来,放在abc文件里面
  // 别人在vue-cli的工具上,又添加了渲染模板;如果是真是的项目开发,可以直接去vue的git地址拉取
  await clone('https://github.com/vuejs/vue-cli.git', name)
}

在执行

mycli init abc

WX20200802-120040@2x.png

下载成功
在目录下创建了abc这个文件

WX20200802-120204@2x.png

安装依赖

安装依赖,就是执行npm install 方法
使用spawn 方法:通过生成一个子进程,去执行指定的命令

child_process.spawn(command[, args][, options])
http://nodejs.cn/api/child_process.html#child_process_child_process_spawn_command_args_options

// 打印欢迎界面
const {clone} =require('./download')
const {promisify} = require('util')

const figlet = promisify(require('figlet'))
const clear = require('clear')
const chalk = require('chalk')

const log=content=>console.log(chalk.green(content))

// -- 改造spawn 
// ------两步操作:
// --------- 1.对接输出流
// --------- 2.promisify,变成promise函数,会方便些,原生的promisify是一个回调函数
const spawn  =async(...args)=>{
  // 原生的spawn 
  const {spawn} = require('child_process')
  // 封装 promise 方法;返回执行承诺
  return new Promise(resolve=>{  // 回调 resolve
    // 1. 定义一个线程 , 子进程
    // --------- 执行spwan 方法,使用的子进程
    // --------- 如果执行参数放在args里面,返回一个子进程
    const proc = spawn(...args)
    //2.子、主进程对接
    // --------- 子进程执行的时候,是有一个输出流,也是一个流,
    // --------- 希望把子进程执行的流和主进程的流,进行对接
    // 对接的好处是,子进程的流都可以在主进程看到
    proc.stdout.pipe(process.stdout) // stdout 正常流
    proc.stderr.pipe(process.stderr) //  stderr 异常流
    // 3.回调
    // --------- 当某一个指令完成的时候,会执行close,比如执行npm install,之后就会执行这个close
    // --------- close执行,之后返回执行承诺
    proc.on('close',()=>{
      resolve()
    })
  })

}
module.exports = async name=>{
  //清屏幕
  clear()
  // 打印
  const data= await figlet('mycli-ohh Welocome')
  log(data)
  // 克隆
  log('🚀创建项目:'+name) //  name是传进来的参数,创建的项目名称
  // 当我们去执行 myclie init abc 时,相当于把项目克隆下来,放在abc文件里面
  await clone('github:su37josephxia/vue-template', name)

  // 安装依赖 执行npm install。 子进程的方式去执行,使用spwan来执行
  // 需要对spawn 进行一个小小的改造;比如安装了什么包,输出可以看到
  log('安装依赖')
  // npm是命令  npm后面接的都是参数 install是参数。所以后面的参数是以数组的形式去表示
  await spawn('npm', ['install'],{cwd: `./${name}`}) 
  // {cwd: `./${name}`} 要求在某一个文件夹下面执行
  // 当我们在执行 ohh init abc; 但是我们实际上是想在abc这个文件下去执行npm install

  // 日志
  log(chalk.green(`
安装完成:
To get Start:
================
    cd ${name}
    npm run serve
================
  `))
}

image

启动项目

利用open函数,启动项目

  const open= require('open')
  // 打开浏览器
  module.exports = async name=>{
    ..........
    // 在安装完成之后
    open(`http://localhost:8080`);
    await spawn('npm', ['run', 'serve'], {cwd:`./${name}`})  
  }

约定路由功能

目的:

当我们在views文件夹中添加新的 vue文件时,
image
router.js,也需要新增加载路径
image
App.vue 中也需要添加菜单

image

新加一个视图有两个地方需要变动,router.js需要加一个项目,App.vue也需要加一项。
接下来需要做的就是让他自动产生,类似于umi的命令

步骤:

1.首先用fs读取views里面的文件,根据文件的列表,自动去生成新的router.js里面新的代码。简单的说就是字符串拼接。复杂一些,可以利用模版工具进行渲染。类似组装vue代码。

  1. 利用模板自动渲染;事先设置两个template模板,App.vue 和 router.js;这两个代码模板。模板-router.js里面进行拼装
  2. 把渲染好的代码放在指定位置即可

abc/template/App.vue.hbs

<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">Home</router-link> 
      {{#each list}}
      | <router-link to="/{{name}}">{{name}}</router-link>
      {{/each}}
    </div>
    <router-view/>
  </div>
</template>

<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>

abc/template/router.js.hbs

import Vue from 'vue'
import Router from 'vue-router'
import Home from './views/Home.vue'

Vue.use(Router)

export default new Router({
  mode: 'history',
  base: process.env.BASE_URL,
  routes: [
    {
      path: '/',
      name: 'home',
      component: Home
    },
    {{#each list}}
    {
      path: '/{{name}}',
      name: '{{name}}',
      component: () => import('./views/{{file}}')
    },
    {{/each}}
  ]
})

lib/refresh.js

const fs = require('fs')
// Handlebars 是一种简单的 模板语言。
// 它使用模板和输入对象来生成 HTML 或其他文本格式。Handlebars 模板看起来像常规的文本,但是它带有嵌入式的 Handlebars 表达式 。
const handlebars = require('handlebars')

// 粉笔
const chalk = require('chalk')

module.exports = async()=>{
  // 获取列表
  const list =fs.readdirS~~~~ync('./src/views')
        .filter(v=>v!=='Home.vue')
        .map(v=>({
          name:v.replace('.vue', '').toLowerCase(),
          file:v
        }))
       
        // 生成路由
        compile({list}, './src/router.js', './template/router.js.hbs')
        //生成菜单
        compile({list}, './src/App.vue', './template/App.vue.hbs')

        /**
         * 编译模板
         * @param {*} meta 数据
         * @param {*} filePath  目标文件
         * @param {*} templatePath  模板文件 通过什么去编译
         */
        function compile(meta, filePath,templatePath){
          // 判断模板是否 存在
           // 读成一个字符串
           const content = fs.readFileSync(templatePath).toString()
           //-柯理化方法;两层 
           // -------1.生成编译 输出 生成编译函数(生成代码的代码函数),类似于vue 的compile生成的是render函数;
           // -------2. meta 真正的数据
           const result= handlebars.compile(content)(meta)

           // 写入
           fs.writeFileSync(filePath, result)
           console.log(chalk.green(`🚀 ${filePath} 创建成功`))
        }

}

bin/mycli.js

#!/usr/bin/env node 
// 以sheel 脚本的形式来执行
// 执行解释器类型是node。使用node去执行
// 程序执行入口bin
// 整个程序入口文件mycli

// console.log('cli.....')

// 定制命令行界面的介绍
// 比如我们执行 vue cli 的时候,出来的命令行界面
const program = require('commander')

// -v 时候 的版本号。我们直接用package上的version字段
program.version(require('../package.json').version)

// 创建,命令,命令行定制 
// <name> 代表参数
program
    .command('init <name>')
    .description('init project') //描写
    .action(require('../lib/init')
    )
program
    .command('refresh')
    .description('refresh routers..')
    .action(require('../lib/refresh'))

    console.log('process.argv', process.argv)
  program.parse(process.argv)// 现在进程的argv,最后一行是固定的

image

查看原文

赞 0 收藏 0 评论 0

哦哈哈 发布了文章 · 8月17日

egg.js 原理解析

Egg.js 介绍

基于Koa的企业级三层结构框架

Egg.js 的结构

三层结构

  • 信息资源层
暴露给外面的接口,后端对其他服务的暴露,包含 视图、接口;
  • 业务逻辑层
重复的业务逻辑处理放在里面,实现核心的业务控制也是在这一层实现。
  • 数据访问层
重点负责数据库访问,完成持久化功能

项目结构

Egg-Dome
├──app 
├────controller #路由对应的加载文件
├────public #存放静态资源
├────service #重复的业务逻辑处理
├────router.js #路径匹配
├──config #配置文件

基本使用

app/controller/product.js
const {Controller} = require('egg')

class ProductController extends Controller {
 async index(){
   const {ctx} = this
   const res = await ctx.service.product.getAll()
   ctx.body=res
 }
app/service/product.js
const {Service} = require('egg');
class ProductService extends Service{
  async getAll(){
    return {
      id: 1,
      name: '测试数据'
    }
  }
}

module.exports = ProductService

app/router.js

'use strict';

/**
 * @param {Egg.Application} app - egg application
 */
module.exports = app => {
  const { router, controller } = app;
  router.get('/product',controller.product.index)
};

创建模型层

egg.js 加载任何功能都是基于插件
以mysql + sequelize为例例演示数据持久化
安装:

npm install --save egg->sequelize mysql2

环境配置

config/plugin.js
'use strict';

module.exports={
  sequelize= {
    enable: true,
    package: 'egg-sequelize',
  }
}
config/config.default.js
  // add your user config here
  const userConfig = {
    sequelize: {
      dialect: "mysql",
      host: "127.0.0.1",
      port: 3306,
      username: "root",
      password: "example",
      database: "ohh"
    }
  };

实现分层框架

创建一个ohh-egg-dome的项目
先 npm init一下,创建下项目
目录结构:

ohh-egg-dome
├── routers
├──── index.js
├──── user.js
├── index.js

自动加载路由功能

首先创建两个路径的文件

routes/index.js
module.exports = {
  'get /': async ctx =>{
    ctx.body = '首页'
  },
  'get /detail': async ctx=>{
    ctx.body = '详细页面'
  }
}
routes/user.js
module.exports={
  // user/
  'get /': async ctx=>{
    ctx.body= '用户首页'
  },
  // user/info
  'get /info': async ctx=>{
    ctx.body = '用户详情页'
  }
}

配置路由页面
让上面写的两个路由,index和user自动加载
ohh-loader.js

const fs = require('fs')
const path= require('path')
const Router = require('koa-router')


function load(dir, cb){
  // 加工成绝对路径
  const url = path.resolve(__dirname, dir)
  // 列表
  const files = fs.readdirSync(url)

  // 遍历
  files.forEach(filename=>{
    // 将 user.js 中的js去掉
    filename = filename.replace('.js','')
    const file = require(url+'/'+filename)

    cb(filename,file)
  })
}

function initRouter(){
  const router =new Router()
  load('routes',(filename, routes)=>{
    // 是index 路径直接是/ 如果是别的前缀,那么就需要拼接: /user
    const prefix =filename ==='index'?'':`/${filename}`

    Object.keys(routes).forEach(key=>{
      const [method,path] = key.split(' ')
      console.log(`正在映射地址: ${method.toLocaleUpperCase()} ${prefix}${path}`)
      router[method](prefix + path, routes[key])
    })
  })
  return router
}

module.exports = {initRouter}

index.js

const app = new(require('koa'))()
const {initRouter} = require('./ohh-loader')
app.use(initRouter().routes())
app.listen(7001)

运行

nodemon index.js

image

页面展示
image

如何封装

封装

const {initRouter} = require('./ohh-loader')
app.use(initRouter().routes())

这属于架构的一部分,不单是加载路由而是加载多层操作,所以把实现细节需要封装起来。

ohh.js

const Koa = require('koa')
const {initRouter} = require('./ohh-loader')

class ohh{
  constructor(conf){
    this.$app = new Koa(conf)
    this.$router =initRouter()
    this.$app.use(this.$router.routes())
  }
  start(port){
    this.$app.listen(port, ()=>{
      console.log('服务启动 at '+ port);
    })
  }
}

module.exports = ohh
index.js
// const app = new(require('koa'))()
// const {initRouter} = require('./ohh-loader')
// app.use(initRouter().routes())
// app.listen(7001)


const ohh =require('./ohh')
const app =new ohh()
app.start(7001)

controller功能实现

添加文件
ohh-egg-dome
├── controller
├──── home.js
controller/home.js
module.exports = {
  index: async ctx=>{
    ctx.body = "柯里化-首页"
  },
  detail:ctx=>{
    ctx.body="柯里化-详请页面"
  }
}

routes/index.js

module.exports= app => ({
 'get /': app.$ctrl.home.index,
  'get /detail': app.$ctrl.home.detail
})
// 将对象转化成=> 对象工厂
ohh-loader.js
const fs = require('fs')
const path= require('path')
const Router = require('koa-router')


function load(dir, cb){
  // 加工成绝对路径
  const url = path.resolve(__dirname, dir)
  // 列表
  const files = fs.readdirSync(url)
  // 遍历
  files.forEach(filename=>{
    // 将 user.js 中的js去掉
    filename = filename.replace('.js','')
    const file = require(url+'/'+filename)

    cb(filename,file)
  })
}

function initRouter(app){
  const router =new Router()
  load('routes',(filename, routes)=>{
    // 是index 路径直接是/ 如果是别的前缀,那么就需要拼接: /user
    const prefix =filename ==='index'?'':`/${filename}`
    // 判断传进来的是柯里化函数 
    routes = typeof routes === 'function' ? routes(app): routes
    Object.keys(routes).forEach(key=>{
      const [method,path] = key.split(' ')
      console.log(`正在映射地址: ${method.toLocaleUpperCase()} ${prefix}${path}`)
      router[method](prefix + path, routes[key])
    })
  })
  return router
}
// controller 加载进来
function initController(){
  const controllers={}
  load('controller', (filename, controller)=>{
    controllers[filename] = controller
  })
  return controllers
}

module.exports = {initRouter, initController}
ohh.js
const Koa = require('koa')
const {initRouter, initController} = require('./ohh-loader')

class ohh{
  constructor(conf){
    this.$app = new Koa(conf)
    this.$ctrl =initController() //新加入进来的controller
    this.$router =initRouter(this) // 把this传进来,在
    this.$app.use(this.$router.routes())
  }
  start(port){
    this.$app.listen(port, ()=>{
      console.log('服务启动 at '+ port);
    })
  }
}

module.exports = ohh

image

service使用

创建service
service/user.js
//  模拟异步
const delay = (data, tick)=> new Promise (resolve => {
  setTimeout(()=>{
    resolve(data)
  },tick)
})


module.exports = {
  getName(){
    return delay('ohh', 1000)
  },
  getAge(){
      return 18
  }
}
routes/user.js
module.exports={
  // user/
  // 'get /': async ctx=>{
  //   ctx.body= '用户首页'
  // },
  'get /':async app=>{
    const name= await app.$service.user.getName()
    app.ctx.body= '用户:'+ name
  },

  // user/info
  // 'get /info': async ctx=>{
  //   ctx.body = '用户详情页'
  // }
  'get /info': async app=>{
    app.ctx.body="年龄:"+  app.$service.user.getAge()
  }
}
controller/home.js
module.exports = app=>({
  // index: async ctx=>{
  //   ctx.body = "柯里化-首页"
  // },
  // 这里需要用上app, 所以需要整个对象进行升阶
  index: async ctx=>{
    const name= await app.$service.user.getName()
    app.ctx.body = 'ctrl usr '+ name
  },

  detail:ctx=>{
    ctx.body="柯里化-详请页面"
  }
})
service 文件加载
ohh-loader.js
const fs = require('fs')
const path= require('path')
const Router = require('koa-router')


function load(dir, cb){
  // 加工成绝对路径
  const url = path.resolve(__dirname, dir)
  // 列表
  const files = fs.readdirSync(url)
  // 遍历
  files.forEach(filename=>{
    // 将 user.js 中的js去掉
    filename = filename.replace('.js','')
    const file = require(url+'/'+filename)
    cb(filename,file)
  })
}

function initRouter(app){
  const router =new Router()
  load('routes',(filename, routes)=>{
    // 是index 路径直接是/ 如果是别的前缀,那么就需要拼接: /user
    const prefix =filename ==='index'?'':`/${filename}`
    // 判断传进来的是柯里化函数
    routes = typeof routes === 'function' ? routes(app): routes
    Object.keys(routes).forEach(key=>{
      const [method,path] = key.split(' ')
      console.log(`正在映射地址: ${method.toLocaleUpperCase()} ${prefix}${path}`)
      // 这里进行了; 一次调整。router 中的文件需要用ctx 。所以包装了一层
      router[method](prefix + path, async ctx=>{
      // app.ctx, 需要重新复制,因为app中的ctx是ohh.loader的;
      // 因为在router中,我们使用的是 `app.ctx.body= '用户:'+ name`,这个ctx就是Koa的
        app.ctx =ctx
        await routes[key](app)
      })
    })
  })
  // console.log('router', router);
  return router
}
// controller 加载进来
function initController(app){
  const controllers={}
  load('controller', (filename, controller)=>{
    // console.log('controller-filename', filename, controller);
    controllers[filename] = controller(app)
  })
  console.log(controllers,'controllers')
  return controllers
}

// 加载service文件
function initService(){
  const services={}
  // filename 在service 中的文件名称; 
  // service  在文件中默认导出对象的内容
  load('service',(filename,service)=>{
    services[filename]= service
  })
  return services
}

module.exports = {initRouter, initController, initService}
ohh.js
const Koa = require('koa')
const {initRouter, initController, initService} = require('./ohh-loader')

class ohh{
  constructor(conf){
    this.$app = new Koa(conf)
    this.$service = initService() 
    this.$ctrl =initController(this)
    this.$router =initRouter(this) // 把this传进来,在
    this.$app.use(this.$router.routes())
  }
  start(port){
    this.$app.listen(port, ()=>{
      console.log('服务启动 at '+ port);
    })
  }
}

module.exports = ohh
在controller中使用service,需要将controller进行升阶,柯理化
router中使用service,改为函数,需要内部用的app

加载数据层

首先安装sequelize、mysql2

npm install sequelize mysql2 --save
config/index.js
// 数据库配置
module.exports = {
  db:{
    dialect:'mysql',
    host:'localhost',
    database:'ohh',
    username:'root',
    password:'example'
  } 
}
ohh-loader.js
// 主要功能是自动加载config函数,判断里面是有数据库相应的配置,自动初始化数据库
const Sequelize = require('sequelize')
function loadConfig(app){
  load('config', (filename,conf)=>{
     if(conf.db){
       console.log('加载数据库');
        app.$db = new Sequelize(conf.db)
     }
  })
}
module.exports = {initRouter, initController, initService, loadConfig}
const Koa = require('koa')
const {initRouter, initController, initService, loadConfig} = require('./ohh-loader')

class ohh{
  constructor(conf){
    loadConfig(this)
  }
  start(port){
    this.$app.listen(port, ()=>{
      console.log('服务启动 at '+ port);
    })
  }
}

module.exports = ohh

image

加载数据模型

model/user.js
const {STRING} = require("sequelize");
// 新建数据库模型
module.exprots={
  schema:{
    name: STRING(30)
  },
  options:{
    timestamps: false
  }
}
ohh-loader.js
// 主要功能是自动加载config函数,判断里面是有数据库相应的配置,自动初始化数据库
const Sequelize = require('sequelize')
function loadConfig(app){
  load('config', (filename,conf)=>{
     if(conf.db){
       console.log('加载数据库');
        app.$db = new Sequelize(conf.db)
        // 加载模型
        app.$model = {} 
        load('model',(filename, {schema, options})=>{
          // 创建模型
          app.$model[filename] = app.$db.define(filename,schema,options)
        })
        app.$db.sync()
     }
  })
}
ohh.js
class ohh{
  this.$service = initService(this)   
}
ohh-loader.js
//service  使用model
// 加载service文件
function initService(app){
  const services={}
  // filename 在service 中的文件名称; 
  // service  在文件中默认导出对象的内容
  load('service',(filename,service)=>{
    services[filename]= service(app)
  })
  return services
}
service/user.js
module.exports = app=>({
  getName(){
    // return delay('ohh', 1000)
    return app.$model.user.findAll()
  },
  getAge(){
      return 18
  }
})
controller/home.js
  index: async ctx=>{
    const name= await app.$service.user.getName()
    app.ctx.body = name
  },

自动加载中间件

middleware/logger.js
module.exports= async (ctx,next)=>{
  console.log(ctx.method+" "+ctx.path);
  const start =new Date()
  await next()
  const duration = new Date() - start;
  console.log(ctx.method+" "+ctx.path+" "+ctx.status+" "+duration+"ms");
}
config/index.js
// 数据库配置
module.exports = {
  db:{
    dialect:'mysql',
    host:'localhost',
    database:'ohh',
    username:'root',
    password:'example'
  },
  // 中间件配置,定义为数组
  middleware:[
    // 中间件的名字
    'logger'
  ]
}
ohh-loader.js
function loadConfig(app){
  load('config', (filename,conf)=>{
     // 如果有中间件配置
     if(conf.middlerware){
      // 不需要load,不用全部加载。所以依次加载

      // 首先处理绝对路径
      // 三段体, /xxx+ '/middleware/'+ 'logger'
      const midPath =path.resolve(_dirname,'middleware', mid)
      app.$app.use(require(midPath))
     }
    })


### 定时任务
使⽤Node-schedule来管理定时任务

npm install node-schedule --save


> `schedule/log.js`

module.exports= {
// 时间间隔 crontab 时间间隔的字符串
// */ 表示先执行一次,3秒后,在执行。
interval:'/3 ', // 3秒执行一次
handler(){

console.log('定时任务,每三秒执行一次'+ new Date());

}
}

> `schedule/user.js`

module.exports={
// 30秒之后执行
interval:"30 *",
handler(){

console.log('定时任务 每分钟30秒执行一次'+ new Date())

}
}

> `ohh-loader.js`

const schedule = require('node-schedule')
function initschecule(){
load('schedule',(filename, scheduleConfig)=>{

schedule.scheduleJob(scheduleConfig.interval,  scheduleConfig.handler)

})
}


> `ohh.js`

class ohh {

constructor(conf){
    initschecule()
}

}


> linux的crobtab 的介绍说明
> [https://www.runoob.com/w3cnote/linux-crontab-tasks.html](https://www.runoob.com/w3cnote/linux-crontab-tasks.html)
查看原文

赞 1 收藏 0 评论 0

认证与成就

  • 获得 8 次点赞
  • 获得 1 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 1 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 6月3日
个人主页被 808 人浏览