yaoxfly

yaoxfly 查看完整档案

福州编辑  |  填写毕业院校  |  填写所在公司/组织 github.com/yaoxfly 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

yaoxfly 发布了文章 · 10月9日

走近Ts,用了爽,用后一直爽(一)

前言

vue3已经发布了,ts的脚步已经阻拦不住了,还只会es6?别想了,人家都已经在行动了,以下是ts的基本系列教程,ts的基本语法,高级语法等,以及在vue项目中如何应用ts,跟着我赶紧撸起来吧。

基本数据类型

数字

const a: number = 3;

字符串

const b: string = "1";

数组

const c: number[] = [1, 2, 3];
const d: Array<number> = [1, 3];
const arr: any[] = [1, "33", true];

元组

可以为数组中的每个参数定义相对应的类型

const e: [number, string] = [1, "ww"];

枚举

enum error {
  blue = 3,
  "orange",
}
const f: error = error.orange;
console.log(f); //输出4

tips

  1. 如果未赋值的上一个值是数字那么这个未赋值的值的是上一个值的值+1
  2. 如果未赋值上一个值未赋值那么输出的就是它的下标
  3. 如果未赋值的上一个值的值是非数字,那么必须赋值

布尔类型

const g: boolean = true;

对象


const i: object = {};

undefined

常用于组合类型

let j: number | undefined;

null

let k: null;

void

指定方法类型,表示没有返回值,方法体中不能return

function aa(): void {
  console.log(1);
}

//如果方法有返回值,可以加上返回值的类型
function bb(): number {
  return 1;
}

never

其他类型 (包括null和undefined)的子类型,代表从不会出现的值

let l: never;

//匿名函数并抛出异常
l = (() => {
  throw new Error("111");
})();

任意类型

让参数可以是任何一种类型


let h: any = 1;
h = true;
h = "st";

函数

函数申明

function cc(): void {}

方法传参

function getUserInfo(name: string, age?: number, school: string = "清华大学") {
  return `name:${name}--age:${age}--school:${school}`;
}
tips: ?代表这个参数可传可不传,不传就是undefined,也可定义个默认的值

剩余参数

传递多个时,如果用了剩余参数,就可以把未定义的形参转换为数组。

function sum (a: number, b: number, ...arr: number[]): number {
  let sum: number = a + b;
  arr.forEach((element) => {
    sum += element;
  });
  console.log(arr); [3,4,5]  
  return sum;
}
console.log(sum(1, 2, 3, 4, 5)); //15

函数重载

function reload(name: string): string;
function reload(age: number): string;
function reload(param: any): any {
  return typeof param === "string" ? `我是:${param}` : `我的年龄:${param}`;
}
console.log(reload(18)); //年龄
tips: 被重载的方法,是没有方法体,可以根据参数的类型走其中一个方法并判断参数,但如果传入的参数类型不是任何被重载方法的参数类型就不允许通过。
 第 1 个重载(共 2 个),“(name: string): string”,出现以下错误。
   类型“never[]”的参数不能赋给类型“string”的参数。
 第 2 个重载(共 2 个),“(age: number): string”,出现以下错误。
   类型“never[]”的参数不能赋给类型“number”的参数


class Person {
  // 私有变量
  private name: string;
  
  // 构造函数
  constructor(name: string) {
    this.name = name;
  }
  
  // 获取名字
  getName(): string {
    return this.name;
  }
  
  // 设置名字
  setName(name: string): void  {
    this.name = name;
  }
}

let p = new Person("张三");
p.setName("李四");
console.log(p);

继承

class Son extends Person {
 // 静态属性
  public static age: number = 18;
  // 学校
  public school: string;
  //构造方法
  constructor(name: string, school: string) {
    // 访问派生类的构造函数中的 "this" 前,必须调用 "super",初始化父类构造函数 --并把参数传给父类
    super(name); 
    //把传进来的school赋值给全局变量
    this.school = school;
  }
  //静态方法
  static run(name: string): string {
    return `${name}在跑步,他的年龄才${this.age}`;
  }
}

let son = new Son("王五", "清华大学");
son.setName("赵六"); // 私有类也不能在子类的外部访问,但可通过公开的方法中进行赋值和访问
console.log(son);
console.log(Son.run("方七"));
console.log(Son.age);

tips:

  1. public 在当前类里面,子类,类外面都可以访问
  2. protected 在当前类和子类内部可以访问,类外部无法访问
  3. private 在当前类内部可访问,子类,类外部都无法访问。
  4. 属性不加修饰符,默认就是公有的 (public)

多态

通过抽象方法/方法重载--实现多态--多态的作用是用来定义标准

// 抽象父类
abstract class Animal {
  private name: string;
  constructor(name: string) {
    this.name = name;
  }
  //抽象成员--方法
  abstract eat(): any;
  //抽象成员--属性
  protected abstract ages: Number;
  sleep(): void {
    console.log("睡觉");
  }
}

class cat extends Animal {
  ages: Number = 2;
  constructor(name: string) {
    super(name);
  }
  //非抽象类“cat”不会自动实现继承自“Animal”类的抽象成员“eat”,  必须手动定义父类中的抽象方法--多态
  eat(): string {
    return "猫吃鱼";
  }

  //多态
  sleep(): string {
    return "猫在睡觉";
  }
}

console.log(new cat("33").sleep());

tips:

  1. 抽象类无法实例化
  2. 非抽象类继承抽象父类时不会自动实现来自父类的抽象成员,必须手动定义父类中的抽象成员,否则报错。
  3. 抽象成员包括属性方法

接口

  在面向对象的编程中,接口是一种规范的定义,它定义了行为和动作的规范,

  在程序设计里面,接口起到一种限制和规范的作用。

  接口定义了某一批类所需要遵守的规范,接口不关心这些类的内部状态数据,也不关心这些类里方法的实现细节,它只规定这批类里必须提供某些方法,提供这些方法的类就可以满足实际需要。ts中的接口类似于java,同时还增加了更灵活的接口类型,包括属性、函数、可索引和类等。

属性接口

interface InterfaceName {
  first: string;
  second?: string; //加个问号,接口属性就可以变成可传可不传了,不传默认是undefined。
}
//打印变量
function logParam(name: InterfaceName): void {
  console.log(name.first, name.second, 11);
}
//定义参数
const obj = { first: "1", second: "fff", three: 1 };
//logParam({ first: "1", second: "1", three: 1 }); //报错,只能传接口定义的值
logParam(obj);
tips: 用个变量来存储传入的变量,这样可以传入定义的接口以外的值,否则如果直接传入对象中无接口定义的值会报错,所以建议接口定义了哪些值就传哪些值。

函数类型接口

对方法传入的参数类型,以及返回值类型进行约束,可批量进行约束。

interface keyMap {
  (key: string, value: string): string;
}
let logKeyMap: keyMap = function (key1: string, value: string): string {
  return key1 + value;
};
console.log(logKeyMap("key1", "value"));
tips: 接口只对传入的参数的类型和参数的个数进行约束,不对参数名称进行约束。

可索引接口

  • 约束数组
interface Arr {
  [index: number]: string;
}
let ss: Arr = ["2121"];
  • 约束对象
interface Obj {
  [index: string]: string;
}

let interfaceArr: Obj = { aa: "1" };

tips:

  1. 数组进行约束,index后必须跟着number类型。
  2. 对象进行约束,index后必须跟着string类型
  3. 索引签名参数类型必须为 "string" 或 "number"

类类型接口

  • 进行约束,类似抽象类的实现。
interface Animals {
  name: string;
  eat(): void;
}

class Dogs implements Animals {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  eat() {}
}
  • 接口继承--接口可以继承接口
interface Dog {
  eat(): void;
}

interface Persons extends Dog {
  work(): void;
}

class Cat {
  code() {
    console.log("猫在敲代码");
  }
}

//可继承类后再实现接口
class SuperMan extends Cat implements Persons {
  eat(): void {
    console.log(1);
  }
  work(): void {
    console.log(2);
  }
}
let superMan = new SuperMan();
superMan.code();
tips: 类接口会对类的属性方法进行约束,类似非抽象类继承抽象类时必须实现某些方法和属性,但对属性和方法的类型的约束更加严格,除了方法void类型可被重新定义外,其他属性或方法的类型定义需要和接口保持一致。

泛型

  软件工程中,我们不仅要创建一致的定义良好的api,同时也要考虑可重用性。
组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这在创建大型系统时为你提供了十分灵活的功能

   泛型就是解决接口方法复用性,以及对不特定数据类型的支持。

  要求:传入的参数和返回的参数一致

函数的泛型

function getDate<T>(value: T): T {
  return value;
}
console.log(getDate<number>(123));
tips: 这里的T可改成其他任意值但定义的值,和传入的参数以及返回的参数是一样的,一般默认写法是T,也是业内规范的选择。

类的泛型

class MinClass<T> {
  public list: T[] = [];
  //添加
  add(value: T): void {
    this.list.push(value);
  }
  
  //求最小值
  min(): T {
    //假设这个值最小
    let minNum = this.list[0];
    for (let i = 0; i < this.list.length; i++) {
    //比较并获取最小值
    minNum = minNum < this.list[i] ? minNum : this.list[i];
    }
    return minNum;
  }
}
//实例化类 并且指定了类的T的类型是number
let minClass = new MinClass<number>(); 
minClass.add(23);
minClass.add(5);
minClass.add(2);
console.log(minClass.min());
 //实例化类 并且指定了类的T的类型是string,则其方法的传参和返回都是string类型
let minClass2 = new MinClass<string>();
minClass2.add("23");
minClass2.add("5");
minClass2.add("2");
console.log(minClass2.min());

接口的泛型

  • 第一种写法
interface ConfigFn {
  //规范参数类型,返回值类型
  <T>(value: T): T;
}

let getData: ConfigFn = function <T>(value: T): T {
  return value;
};

console.log(getData<string>("z11"));
  • 第二种写法

interface ConfigFn<T> {
  //参数类型 ,返回值类型
  (value: T): T;
}

//接口方法
function getData<T>(value: T): T {
  return value;
}

//使用接口
let myGetDate: ConfigFn<string> = getData;
console.log(myGetDate("3"));
tips:接口的泛型只针对函数类型的接口

类当做参数传入泛型类

//用户类--和数据库表字段进行映射
class User {
  username: string | undefined;
  password: string | undefined;
  //构造函数-初始化参数
  constructor(param: {
    username: string | undefined;
    password?: string | undefined;
  }) {
    this.username = param.username;
    this.password = param.password;
  }
}


//数据库类
class Db<T> {
  add(user: T): boolean {
    console.log(user);
    return true;
  }
  updated(user: T, id: number): boolean {
    console.log(user, id);
    return true;
  }
}

let u = new User({
  username: "张三",
});

//u.username = "李四";
u.password = "111111";
let db = new Db<User>();
db.add(u);
db.updated(u, 1);
tips: 类的参数名和类型都做了约束。

模块

  内部模块称为命名空间,外部模块简称为模块,模块在其自身的作用域里执行,而不是在全局作用域里;

  这意味着定义在一个模块里的变量、函数、类等等在模块外部是不可见的,除非你明确的使用export形式之一导出它们。

  相反,如果想使用其它模块导出的变量,函数,类,接口等的时候,你必须要导人它们,可以使用import形式之一。

  我们可以一些公共的功能单独抽离成一个文件作为一个模块。
模块里面的变量、函数、类等默认是私有的,如果我们要在外部访问模块里面的数据(变量、函数、类)
我们需要通过export暴露模块里面的数据(变量、函数、类...)。
暴露后我们通过import引入模块就可以使用模块里面暴露的数据(变量、函数、类...)

//modules/db.ts
function getDate(): any[] {
  console.log("获取数据");
  return [
    {
      userName: "张三",
    },

    {
      userName: "李四",
    },
  ];
}

//一个模块里面可以用多次
// export { getDate };
//一个模块里面只能用一次
export default getDate;
 import { getDate as getDbDate } from "./modules/db";
 import getDbDate from "./modules/db";
 getDbDate();
tips: 这个调试时浏览器中不能直接使用,可在nodeweakpack的环境中调试。

命名空间

  在代码量较大的情况下,为了避免各种变量命名相冲突,可将相似功能的函数、类、接口等放置到命名空间内
TypeScript的命名空间可以将代码包裹起来,只对外暴露需要在外部访问的对象。

   命名空间和模块的区别

  • 命名空间:内部模块,主要用于组织代码,避免命名冲突。
  • 模块:ts外部模块的简称,侧重代码的复用,一个模块里可能会有多个命名空间。
 // modules/Animal.ts
export namespace A {
  interface Animal {
    name: String;
    eat(): void;
  }

  export class Dog implements Animal {
    name: String;
    constructor(theName: string) {
      this.name = theName;
    }
    eat() {
      console.log("我是" + this.name);
    }
  }
}

export namespace B {
  interface Animal {
    name: String;
    eat(): void;
  }

  export class Dog implements Animal {
    name: String;
    constructor(theName: string) {
      this.name = theName;
    }
    eat() {}
  }
}
 import { A, B } from "./modules/Animal";
 let ee = new A.Dog("小贝");
 ee.eat();

装饰器

  • 类装饰器:类装饰器在类申明之前被申明(紧靠着类申明),类装饰器应用于类构造函数,可以用于监视,修改或者替换类定义。
function logClass(params: any) {
  console.log(params);
  //params 就是指代当前类--HttpClient
  params.prototype.apiUrl = "动态扩展属性";
  params.prototype.run = function () {
    console.log("动态扩展方法");
  };
  params.prototype.getDate = function () {
    console.log("动态扩展方法2");
  };
}

@logClass
class HttpClient {
  constructor() {}
  getDate() {
    console.log(1);
  }
}

let http: any = new HttpClient();
console.log(http.apiUrl);
http.run();
http.getDate();
tips: 装饰器会覆盖被装饰的类中的方法。
  • 装饰器工厂

可传参的装饰器


function logClassB(param: string) {
  return function (target: any) {
    console.log(target, "装饰器以下的类");
    console.log(param, "装饰器传进来的属性");
  };
}

@logClassB("小慧")
class HttpClients {
  constructor() {}
  getDate() {}
}

let https: any = new HttpClients();
console.log(https);
  • 构造函数装饰器
function logClassC(target: any) {
  console.log(target, 1111);
  //用在这里继承目标类并重载方法和属性
  return class extends target {
    a: any = "我是修改后的属性";
    getDate() {
      console.log(this.a + "--装饰器中的方法输出的");
    }
  };
}

@logClassC
class HttpClient2 {
  public a: string | undefined;
  constructor() {
    this.a = "我是构造函数里面的a";
  }
  getDate() {
    console.log(this.a);
  }
}
const https2 = new HttpClient2();
https2.getDate();

未完待续~

下期预告:在vue中使用ts。

查看原文

赞 23 收藏 17 评论 2

yaoxfly 赞了文章 · 9月28日

vue源码中值得学习的方法

最近在深入研究vue源码,把学习过程中,看到的一些好玩的的函数方法收集起来做分享,希望对大家对深入学习js有所帮助。如果大家都能一眼看懂这些函数,说明技术还是不错的哦。

1. 数据类型判断

Object.prototype.toString.call()返回的数据格式为 [object Object]类型,然后用slice截取第8位到倒一位,得到结果为 Object

var _toString = Object.prototype.toString;
function toRawType (value) {
  return _toString.call(value).slice(8, -1)
}

运行结果测试

toRawType({}) //  Object 
toRawType([])  // Array    
toRawType(true) // Boolean
toRawType(undefined) // Undefined
toRawType(null) // Null
toRawType(function(){}) // Function

2. 利用闭包构造map缓存数据

vue中判断我们写的组件名是不是html内置标签的时候,如果用数组类遍历那么将要循环很多次获取结果,如果把数组转为对象,把标签名设置为对象的key,那么不用依次遍历查找,只需要查找一次就能获取结果,提高了查找效率。

function makeMap (str, expectsLowerCase) {
    // 构建闭包集合map
    var map = Object.create(null);
    var list = str.split(',');
    for (var i = 0; i < list.length; i++) {
      map[list[i]] = true;
    }
    return expectsLowerCase
      ? function (val) { return map[val.toLowerCase()]; }
      : function (val) { return map[val]; }
}
// 利用闭包,每次判断是否是内置标签只需调用isHTMLTag
var isHTMLTag = makeMap('html,body,base,head,link,meta,style,title')
console.log('res', isHTMLTag('body')) // true

3. 二维数组扁平化

vue中_createElement格式化传入的children的时候用到了simpleNormalizeChildren函数,原来是为了拍平数组,使二维数组扁平化,类似lodash中的flatten方法。

// 先看lodash中的flatten
_.flatten([1, [2, [3, [4]], 5]])
// 得到结果为  [1, 2, [3, [4]], 5]

// vue中
function simpleNormalizeChildren (children) {
  for (var i = 0; i < children.length; i++) {
    if (Array.isArray(children[i])) {
      return Array.prototype.concat.apply([], children)
    }
  }
  return children
}

// es6中 等价于
function simpleNormalizeChildren (children) {
   return [].concat(...children)
}

4. 方法拦截

vue中利用Object.defineProperty收集依赖,从而触发更新视图,但是数组却无法监测到数据的变化,但是为什么数组在使用pushpop等方法的时候可以触发页面更新呢,那是因为vue内部拦截了这些方法。

 // 重写push等方法,然后再把原型指回原方法
  var ARRAY_METHOD = [ 'push', 'pop', 'shift', 'unshift', 'reverse',  'sort', 'splice' ];
  var array_methods = Object.create(Array.prototype);
  ARRAY_METHOD.forEach(method => {
    array_methods[method] = function () {
      // 拦截方法
      console.log('调用的是拦截的 ' + method + ' 方法,进行依赖收集');
      return Array.prototype[method].apply(this, arguments);
    }
  });

运行结果测试

var arr = [1,2,3]
arr.__proto__ = array_methods // 改变arr的原型
arr.unshift(6) // 打印结果: 调用的是拦截的 unshift 方法,进行依赖收集

5. 继承的实现

vue中调用Vue.extend实例化组件,Vue.extend就是VueComponent构造函数,而VueComponent利用Object.create继承Vue,所以在平常开发中VueVue.extend区别不是很大。这边主要学习用es5原生方法实现继承的,当然了,es6中 class类直接用extends继承。

  // 继承方法 
  function inheritPrototype(Son, Father) {
    var prototype = Object.create(Father.prototype)
    prototype.constructor = Son
    // 把Father.prototype赋值给 Son.prototype
    Son.prototype = prototype
  }
  function Father(name) {
    this.name = name
    this.arr = [1,2,3]
  }
  Father.prototype.getName = function() {
    console.log(this.name)
  }
  function Son(name, age) {
    Father.call(this, name)
    this.age = age
  }
  inheritPrototype(Son, Father)
  Son.prototype.getAge = function() {
    console.log(this.age)
  }

运行结果测试

var son1 = new Son("AAA", 23)
son1.getName()            //AAA
son1.getAge()             //23
son1.arr.push(4)          
console.log(son1.arr)     //1,2,3,4

var son2 = new Son("BBB", 24)
son2.getName()            //BBB
son2.getAge()             //24
console.log(son2.arr)     //1,2,3

6. 执行一次

once 方法相对比较简单,直接利用闭包实现就好了

function once (fn) {
  var called = false;
  return function () {
    if (!called) {
      called = true;
      fn.apply(this, arguments);
    }
  }
}

7. 对象判断

vue源码中的looseEqual 判断两个对象是否相等,先类型判断再递归调用,总体也不难,学一下思路。

function looseEqual (a, b) {
  if (a === b) { return true }
  var isObjectA = isObject(a);
  var isObjectB = isObject(b);
  if (isObjectA && isObjectB) {
    try {
      var isArrayA = Array.isArray(a);
      var isArrayB = Array.isArray(b);
      if (isArrayA && isArrayB) {
        return a.length === b.length && a.every(function (e, i) {
          return looseEqual(e, b[i])
        })
      } else if (!isArrayA && !isArrayB) {
        var keysA = Object.keys(a);
        var keysB = Object.keys(b);
        return keysA.length === keysB.length && keysA.every(function (key) {
          return looseEqual(a[key], b[key])
        })
      } else {
        /* istanbul ignore next */
        return false
      }
    } catch (e) {
      /* istanbul ignore next */
      return false
    }
  } else if (!isObjectA && !isObjectB) {
    return String(a) === String(b)
  } else {
    return false
  }
}
function isObject (obj) {
  return obj !== null && typeof obj === 'object'
}

由上思路我自己写一个深拷贝方法,不过简单的深拷贝我们可以用 JSON.stringify() 来实现即可。

function deepClone(source) {
  if (!source && typeof source !== 'object') {
    throw new Error('error arguments', 'deepClone')
  }
  const targetObj = source.constructor === Array ? [] : {}
  Object.keys(source).forEach(keys => {
    if (source[keys] && typeof source[keys] === 'object') {
      targetObj[keys] = deepClone(source[keys])
    } else {
      targetObj[keys] = source[keys]
    }
  })
  return targetObj
}

就先分享这些函数,其他函数,后面继续补充,如有不对欢迎指正,谢谢!

查看原文

赞 41 收藏 27 评论 8

yaoxfly 赞了文章 · 9月10日

基于vue脚手架构建库并发布到npm

构建库的常见方法有两种:一种是自己手动构建webpack库打包,设置output为 library; 另一种是基于vue-cli3输出库资源包。我们采用第二种vue脚手架的方式构建库。

新增编译库命令
// package.json 
"scripts": {
    // ...
    "lib": "vue-cli-service build --target lib --name Step --dest dist packages/index.js"
}

// packages/index.js  默认打包Step
import Step from '../steps/src/step';
Step.install = function(Vue) {
    Vue.component(Step.name, Step);
};
export default Step;
  • --name: 库名称。
  • --target: 构建目标,默认为应用模式。这里修改为 lib 启用库模式。
  • --dest: 输出目录,默认 dist。
  • [entry]: 最后一个参数为入口文件,默认为 src/App.vue。

更多详细配置查看 ☛ vue脚手架官网

如果该库依赖于其他库,请在vue.config.js 配置 externals
// vue.config.js
module.exports = {
    configureWebpack:{
      externals: {
         vue: 'Vue',
         'vue-router':'VueRouter',
         axios: 'axios'
      }
    }
}

执行 npm run lib 就可以发现我们的库被打包到了 根目录的dist文件夹下。

添加 .npmignore 文件(可选)

和 .gitignore 的语法一样,具体需要提交什么文件,看各自的实际情况

# 忽略目录
examples/
packages/
public/

# 忽略指定文件
vue.config.js
babel.config.js
*.map

配置npm库信息

配置package.json文件,以发布库文件。

{
  "name": "gis",
  "version": "1.2.5",
  "description": "基于 Vue 的库文件",
  "main": "dist/gis.umd.min.js",
  "keyword": "vue gis",
  "private": false,
   "files": ["dist"],
  "license": "MIT"
 }
  • name: 包名,该名字是唯一的。可在 npm 官网搜索名字,如果存在则需换个名字。
  • version: 版本号,每次发布至 npm 需要修改版本号,不能和历史版本号相同。
  • description: 描述。
  • main: 入口文件,该字段需指向我们最终编译后的包文件。
  • keyword:关键字,以空格分离希望用户最终搜索的词。
  • author:作者
  • files: 要上传的文件
  • private:是否私有,需要修改为 false 才能发布到 npm
  • license: 开源协议
  • dependencies: 依赖库
注意每次发布新的库,需要更改版本号,规则如下:
"version": "1.2.5" 主版本号为 1,次版本号 2,修订号 5
  • 主版本号(Major):当你做了不兼容的API修改
  • 次版本号(Minor):当你做了向下兼容的功能性新增
  • 修订号(Patch):当你做了向下兼容的问题修正

登录npm

首先设置登录的npm镜像地址

npm config set registry http://168.20.20.57.4873

然后在终端执行登录命令,输入用户名、密码、邮箱即可登录

npm login

接着发布库资源到npm

npm publish

最后发布成功可到官网查看对应的包并下载

npm install package_name
查看原文

赞 7 收藏 2 评论 1

yaoxfly 赞了文章 · 8月16日

vue源码分析——从实例化到渲染流程

本文vue 版本为 2.5.17, 分析的是 Runtime + Compiler 构建出来的 Vue.js。 在 Vue.js 2.0 中,最终都是通过 render 函数渲染,如果含有 template 属性,则需要将 template 编译成 render 函数,那么这个编译过程会发⽣运⾏时,所以需要带有Compiler编译器的版本。本文为vue源码介绍系列的第一篇,主要归纳整合vue实例化,将render函数转为vnode到生成挂载真实dom主要流程,具体细节请查看源码。第二篇将介绍组件化过程

vue源码相对比较复杂,需要耐心反复理解及调试,不懂就多调试问百度,罗马不是一日建成的,相信坚持就会有收获哈~

具体调试可以下载vue.js,然后具体做断点debugger调试。

  <script data-original="./vue.js"></script>
  <div id="app"></div>
  <script>
    var vm = new Vue({
      el: '#app',
      render(h) {
        return h(
          'div', { attr: { class: 'classname' } },
          [
            'first item',
            h('h2', { style: {color: 'orange' } }, 'second item')
          ]
        )
      },
      data: {
        message: 'Hello',
      }
    })
  </script>

先上图分析流程

初始化流程图

1. 定义Vue

function Vue (options) {
  if ("development" !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword');
  }
  this._init(options);
}
initMixin(Vue);  // 定义 _init
stateMixin(Vue);  // 定义 $set $get $delete $watch 等
eventsMixin(Vue);   // 定义事件  $on  $once $off $emit
lifecycleMixin(Vue); // 定义 _update  $forceUpdate  $destroy
renderMixin(Vue); // 定义 _render 返回虚拟dom  

2. initMixin

实例化Vue时,执行 _init, _init 定义在 initMixin 中

  Vue.prototype._init = function (options) {
    // 合并 options
    if (options && options._isComponent) {
      initInternalComponent(vm, options); // 组件合并
    } else {
      // 非组件合并
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor), 
        options || {},
        vm
      );
    }
    initLifecycle(vm); // 定义 vm.$parent vm.$root vm.$children  vm.$refs 等
    initEvents(vm);   // 定义 vm._events  vm._hasHookEvent 等
    initRender(vm); // 定义 $createElement $c
    callHook(vm, 'beforeCreate'); // 挂载 beforeCreate 钩子函数
    initInjections(vm); // resolve injections before data/props
    initState(vm);  // 初始化 props methods data computed watch 等方法
    initProvide(vm); // resolve provide after data/props
    callHook(vm, 'created'); // 挂载 created 钩子函数
    if (vm.$options.el) {
      vm.$mount(vm.$options.el); // 实例挂载渲染dom
    }
  };

3. $mount

vue最终都是通过render函数将dom编译为虚拟dom

// 构建render函数
if (!options.render) {
  // 如果没有render属性,那么将template模版编译转为render
}

// 最后调用 mount
return mount.call(this, el, hydrating)

// mount 调用 mountComponent
return mountComponent(this, el, hydrating)

4. mountComponent

通过 new Watcher 调用执行 updateComponent, vm._render获取虚拟dom, vm._update将虚拟dom转为真实的dom并挂载到页面

// hydrating 代表服务端渲染 hydrating => false
updateComponent = function () {
  vm._update(vm._render(), hydrating); // 关键点
};

5. _render

_render执行render函数 返回vnoe

Vue.prototype._render = function () {
    // 此处的 vm._renderProxy 等价于 vm
    vnode = render.call(vm._renderProxy, vm.$createElement);
}

$createElement 主要是参数重载,整合为统一格式后调用 _createElement函数

function createElement ( context, tag, data,  children,  normalizationType, alwaysNormalize) {
  // 参数重载
  if (Array.isArray(data) || isPrimitive(data)) {
    normalizationType = children;
    children = data;
    data = undefined;
  }
  if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE;
  }
  return _createElement(context, tag, data, children, normalizationType)
}

_createElement 主要是根据 tag 标签判断是组件还是普通node标签,返回对应的vnode虚拟dom

function _createElement ( context, tag, data, children, normalizationType) {
  if (typeof tag === 'string') {
    // platform built-in elements
    vnode = new VNode(
      config.parsePlatformTagName(tag), data, children,
      undefined, undefined, context
    );
  }else{
    // direct component options / constructor
    vnode = createComponent(tag, data, context, children);
  }
}

6. _update

_update 主要实现 vnode 转化为实际的dom, 注入到页面的同时并销毁页面模版。

定义 _update

//  _update => __patch__
Vue.prototype._update = function (vnode, hydrating) {
  if (!prevVnode) {
    // 初始化时
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
  } else {
    // 更新时
    vm.$el = vm.__patch__(prevVnode, vnode);
  }
}

定义 __patch__

//   __patch__ => patch
Vue.prototype.__patch__ = inBrowser ? patch : noop;

定义 patch,

// 利用函数柯里化,将服务端和浏览器的差异集成到modules, nodeOps为dom元素操作方法集合
var modules = platformModules.concat(baseModules);
var patch = createPatchFunction({ nodeOps: nodeOps, modules: modules });

定义 createPatchFunction

function createPatchFunction (backend) {
  return function patch (oldVnode, vnode, hydrating, removeOnly) {
    // 创建新节点
    createElm(
      vnode,
      insertedVnodeQueue,
      oldElm._leaveCb ? null : parentElm,
      nodeOps.nextSibling(oldElm)
    );
    // 销毁节点
    if (isDef(parentElm)) {
      removeVnodes(parentElm, [oldVnode], 0, 0);
    } else if (isDef(oldVnode.tag)) {
      invokeDestroyHook(oldVnode);
    }
  }
}

定义 createElm, 根据vnode创建真实dom

function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index) {
  // createChildren函数由子到父,深序递归调用createElm
  createChildren(vnode, children, insertedVnodeQueue);
  if (isDef(data)) {
    invokeCreateHooks(vnode, insertedVnodeQueue);
  }
  // 子节点插入到父节点
  insert(parentElm, vnode.elm, refElm);
}

以上就是vue从实例化,到调用render函数生成vnode,vnode通过patch转为真实dom节点,并挂载到页面的流程情况。接下来将第二篇将接扫vue组件化和生命周期过程。

查看原文

赞 13 收藏 12 评论 4

yaoxfly 发布了文章 · 8月13日

vue3.0体验卡

前言

vue3.0 Rc(候选发布版本)已经于7月18上线,离正式版本发布已经不远了,鉴于此,本人就通过@vue/composition-api 这个 Vue Composition API 来事先体验下3.0的新特性,为以后能快速上手新版本做准备。

准备工作

下载与引入

  • 下载体验版api
 npm i @vue/composition-api
  • 引入与使用
 1. 在main.js中要引入全部的api
 
 import VueCompositionApi from '@vue/composition-api'

 Vue.use(VueCompositionApi)
 
 2. 在页面中按需引入api
 
 import { 需要使用的api } from '@vue/composition-api'
tips: main.js和局部页面都需要引入,不能只在页面中引入。

开发与使用

reactive和toRefs

reactive创建响应式数据,toRefs把普通数据转换成响应式数据

<template>
  <div class="home">
    <span>{{name}}</span>
    <span>{{num}}</span>
    <button @click="btn">按钮</button>
    <button @click="btn2">按钮2</button>
  </div>
</template>
<script>
// reactive 创建响应式数据对象 --类似data
import { reactive, toRefs } from '@vue/composition-api'
export default {
  name: 'Home',
  setup () {
    // state对象
    const state = reactive({
      name: 'hello world'
    })
    // modelData 
    const modelData = reactive({
      num: 1
    })
    const btn = () => modelData.num++
    const btn2 = () => {
      state.name = '我是不双向绑定的数据,没有toRefs转换不可更改'
      return state
    }
    return {
      ...state,
      ...toRefs(modelData), //把数据转换为响应式
      btn, // 事件
      btn2
    }
  }
}
</script>

tips:

  1. setup中是没有this
  2. 数据、方法都写在setup里面。
  3. 方法里改变值需return这个值
  4. 用了...运算符后使用reactive创建出来的数据都不是响应式数据了,需要使用toRefs转换为ref()类型的响应式数据

ref(推荐)

    • reactive一样创建响应式数据,但更推荐使用。
    <template>
     <div class="RefCom">
       <span>{{refCount}}</span>
       <button @click="refCount+=1">+1</button>
     </div>
    </template>
    <script>
    
    import { ref, reactive } from '@vue/composition-api'
    export default {
     name: 'RefCom',
     setup (props, { root }) {
       const refCount = ref(0) //创建响应式数据
       console.log(refCount.value)
       const data = reactive({
         refCount
       })
       console.log(data.refCount)
       data.refCount++
       console.log(data.refCount)
       return {
         ...data,
         refCount
       }
     }
    }
    </script>
    • 模板上的ref--获取dom
    //父组件
    <template>
      <div class="Father">
        <h1 ref="h1Ref">父组件</h1>
        <som ref="somRef"></som>
      </div>
    </template>
    <script>
    
    import som from './Som'
    import { ref, onMounted } from '@vue/composition-api'
    export default {
      name: 'Father',
      components: {
        som
      },
      setup (props, { root }) {
        const h1Ref = ref(null) //赋值null
        const somRef = ref(null)
        onMounted(() => {
          console.log(h1Ref.value, 'h1的dom')
          console.log(somRef.value, 'som的dom')
        })
        return {
          h1Ref,  //要和模板上ref值相同
          somRef
        }
      }
    }
    </script>
    
     //子组件
     <template>
      <div class="Som">
        <h3>子组件</h3>
      </div>
    </template>
    
    <script>
    export default {
      name: 'som',
      setup (props, { root }) {}
    }
    </script>

    tips:

    1. ref括号里的值就是refCount的值,括号里的值可以是各种类型的值。
    2. setup要通过xxx.value获取ref转换的值。
    3. 模板中无需通过xxx.value展示数据,直接{{xxx}}即可,在return时已经进行了转换了。
    4. ref包裹创建出来的值是个对象,里面就一个属性value
    5. reactive包裹ref创建的值不需要通过XXX.value访问
    6. 新的ref会覆盖旧的ref的值
    7. 通过isRef可以判断是否是ref创建出来的。

    computed

    计算属性:可创建可读可写的计算属性。

    <template>
      <div class="RefCom">
        <span>原值:{{refCount}}</span> |
        <span>
          计算属性值:{{ onlyReadComputed
          }}
        </span> |
        <button @click="refCount+=1">+1</button>
      </div>
    </template>
    <script>
    
    import { ref, computed } from '@vue/composition-api'
    export default {
      name: 'RefCom',
      setup (props, { root }) {
        const refCount = ref(0)
        // 只读的计算属性
        const onlyReadComputed = computed(() => refCount.value + 1)
        // 可读可写的计算属性
        const rwComputed = computed({
          get: () => refCount.value + 1,
          set: value => {
            refCount.value = value - 1
          }
        })
        console.log(onlyReadComputed, '只读计算属性的值')
        rwComputed.value = 11
        console.log(rwComputed, '可读可写计算属性的值')
        return {
          refCount,
          rwComputed,
          onlyReadComputed
        }
      }
    }
    </script>

    watch

    监听数据的变化

    <template>
     <div class="RefCom">
       <span>{{refCount}}</span>
       <span>{{name}}</span>
       <button @click="stopWatch">停止watch</button>
       <input v-model="inputValue" />
     </div>
    </template>
    <script>
    import { ref, reactive, watch, toRefs } from '@vue/composition-api'
    export default {
     name: 'watch',
     setup (props, { root }) {
       const refCount = ref(0)
       const inputValue = ref('')
       const state = reactive({
         name: '张总'
       })
       /* ---监听单个--- */
    
       // ref
       const stop = watch(
         refCount,
         (newValue, oldValue) => {
           console.log(refCount.value)
           console.log('新值:' + newValue, '旧的值:' + oldValue)
         }
       )
    
       const stopWatch = () => {
         stop()
       }
    
       // reactive
       watch(
         () => state.name,
         (newValue, oldValue) => {
           // console.log(refCount.value)
           console.log('新值:' + newValue, '旧的值:' + oldValue)
         }
       )
    
       /* ---监听多个--- */
       watch(
         [() => refCount, () => state.name],
         ([newRefCount, newName], [oldRefCount, oldName]) => {
           console.log('newRefCount:' + newRefCount.value, 'newName:' + newName)
           console.log('oldRefCount:' + oldRefCount.value, 'oldName:' + oldName)
         }
       )
    
       setTimeout(() => {
         refCount.value++
       }, 1000)
    
       setTimeout(() => {
         state.name = '李总'
       }, 3000)
    
       // 异步打印
       const asyncPrint = (val) => {
         return setTimeout(() => {
           console.log(val)
         }, 1000)
       }
    
       // ref
       watch(
         inputValue,
         (newValue, oldValue, clean) => {
           const timeId = asyncPrint(newValue)
           // 每当数据变化的时候清除定时器
           clean(() => clearTimeout(timeId))
         }
       )
    
       return {
         ...toRefs(state),
         refCount,
         stopWatch,
         inputValue
       }
     }
    }
    </script>

    tips:

    1. refreactive的值的监听方法不同,reactive需用方法返回值,() => xxx,ref可直接使用。
    2. 当监听多个时,不管是ref还是reactive创建的值,都需要用方法返回
    3. 在监听多个值时,用数组来解构新旧值时,新值和旧值分别在不同的数组里,和vue2.x不一样。
    4. watch监听返回新值、旧值时还返回了个函数,当前函数在watch被重复执行stop操作时发生,可做些清除操作。常见应用场景有防抖。
    5. 防抖:就是对于频繁触发的事件添加一个延时同时设定一个最小触发间隔,如果触发间隔小于设定的间隔,则清除原来的定时,重新设定新的定时;如果触发间隔大于设定间隔,则保留原来的定时,并设置新的定时;防抖的结果就是频繁的触发转变为触发一次

    生命周期

    • beforeCreate -> setup()
    • created -> setup
    • beforeMount -> onBeforeMount
    • mounted -> onMounted
    • beforeUpdate -> onBeforeUpdate
    • updated -> onUpdated
    • beforeDestroy -> onBeforeUnmount
    • destroyed -> onUnmounted
    • errorCaptured -> onErrorCaptured
    <template>
      <div class="Father">
     
      </div>
    </template>
    <script>
    
    
    import { onMounted, onUpdated, onBeforeUnmount } from '@vue/composition-api'
    export default {
      name: 'Father',
    
      setup (props, { root }) {
    
        onMounted(() => {
          console.log('onMounted')
        })
    
        onUpdated(() => {
          console.log('onUpdated')
        })
    
        onBeforeUnmount(() => {
          console.log('onBeforeUnmount')
        })
    
      }
    }
    </script>
    

    tips:

    1. 去除了beforeCreatecreated生命周期,直接就在setup中,setup执行顺序 是beforeCreate后,created
    2. 其他生命周期就在原本前加上on,功能没有什么变化,且定义在setup函数中
    3. 推荐请求都放在onMounted

    依赖注入

    • provide
    //父组件
    <template>
      <div class="Father">
        <h1>父组件</h1>
        <button @click="color='red'">红色</button>
        <button @click="color='blue'">蓝色</button>
        <button @click="color='yellow'">黄色</button>
        <som></som>
      </div>
    </template>
    <script>
    
    import som from './Som'
    import { provide, ref } from '@vue/composition-api'
    export default {
      name: 'Father',
      components: {
        som
      },
      setup (props, { root }) {
        const color = ref('red') //响应式的值,父组件修改可影响子孙后代
        //注入值
        provide('color', color)
        return {
          color
        }
      }
    }
    </script>
     //子组件
     <template>
      <div class="Som">
        <h3>子组件</h3>
        <Grandson />
      </div>
    </template>
    
    <script>
    import Grandson from './Grandson'
    export default {
      name: 'som',
      components: {
        Grandson
      },
      setup (props, { root }) { }
    }
    </script>
    • inject
    //孙子组件
    <template>
      <div class="Grandson">
        <h5 :style="{color:color}">孙子组件</h5>
      </div>
    </template>
    
    <script>
    import { inject } from '@vue/composition-api'
    export default {
      name: 'Grandson',
      setup (props, { root }) {
      //接收值
        const color = inject('color') 
        return {
          color
        }
      }
    }
    </script>

    路由跳转

    <template>
      <div class="home">
        <button @click="jump">跳转</button>
      </div>
    </template>
    <script>
    export default {
      name: 'Home',
      setup (props, { root }) {
       const jump = () => root.$router.push('/about')
        return {
          jump
        }
      }
    }
    </script>
    tips: root指代的就是vue对象,即this,且名字是不可更改的。

    props

    
    //父
    <template>
      <div class="about">
        <h1>This is an about page</h1>
        <div>{{num}}</div>
        <button @click="btn">增加</button>
        <HelloWorld msg="我是props传进去的值" />
      </div>
    </template>
    
    
    //子
    <template>
      <div class="hello">
        <h1>{{ msg }}</h1>
      </div>
    </template>
    
    <script>
    export default {
      name: 'HelloWorld',
      props: {
        msg: String
      },
      setup (props) {
        console.log(props)
      }
    }
    </script>
    

    未完待续~~~~

    查看原文

    赞 17 收藏 10 评论 5

    yaoxfly 赞了文章 · 7月18日

    内外网npm私服(verdaccio)搭建及依赖包的上传

    由于公司基于内网开环境开发,所有依赖包必须从外网拷贝到内网,而且当有依赖包变动时,还得再同步更新到内网,无疑降低了开发效率。再者我们也可以将常用开发组件封装发布于自己的npm仓库,方便复用。

    一、外网全局安装 verdaccio

    npm install -g verdaccio

    首先启动 verdaccio,任意控制台执行命令

    verdaccio

    出现以下信息表明安装成功

       warn --- config file  - /home/yg/.config/verdaccio/config.yaml  // 配置文件所在位置
       warn --- Plugin successfully loaded: htpasswd 
       warn --- Plugin successfully loaded: audit 
       warn --- http address - http://localhost:4873/ - verdaccio/3.10.1 // 仓库所在地址 4873表示默认端口

    浏览器中输入地址 http://localhost:4873/,启动服务。这里显示了我之前上传过的组件。

    verdaccio.png

    二、config.yaml配置说明

    # #号后面是注释
    # 所有包的缓存目录
    storage: ./storage
    # 插件目录
    plugins: ./plugins
    
    #开启web 服务,能够通过web 访问
    web:
      # WebUI is enabled as default, if you want disable it, just uncomment this line
      #enable: false
      title: Verdaccio
    #验证信息
    auth:
      htpasswd:
        #  用户信息存储目录
        file: ./htpasswd
        # Maximum amount of users allowed to register, defaults to "+inf".
        # You can set this to -1 to disable registration.
        #max_users: 1000
    
    # a list of other known repositories we can talk to
    #公有仓库配置
    uplinks:
      npmjs:
        url: https://registry.npmjs.org/
    
    packages:
      '@*/*':
        # scoped packages
        access: $all
        publish: $authenticated
    
        #代理 表示没有的仓库会去这个npmjs 里面去找 ,
        #npmjs 又指向  https://registry.npmjs.org/ ,就是上面的 uplinks 配置
        proxy: npmjs
    
      '**':
        # 三种身份,所有人,匿名用户,认证(登陆)用户
        # "$all", "$anonymous", "$authenticated"
    
        #是否可访问所需要的权限
        access: $all
    
        #发布package 的权限
        publish: $authenticated
    
        # 如果package 不存在,就向代理的上游服务发起请求
        proxy: npmjs
    
    # To use `npm audit` uncomment the following section
    middlewares:
      audit:
        enabled: true
    # 监听的端口 ,重点, 不配置这个,只能本机能访问
    listen: 0.0.0.0:4873
    # 允许离线发布
    publish:
      allow_offline: true
    # log settings
    logs:
      - {type: stdout, format: pretty, level: http}
      #- {type: file, path: verdaccio.log, level: info}
    

    需要格外注意的是必须配置 listen: 0.0.0.0:4873 才能运行其他机子访问!!!
    在离线时要发布依赖包必须设置 allow_offline: true !!!

    三、添加用户并设置npm镜像源

    #当前npm 服务指向本地 
    npm set registry http://localhost:4873
    # 注册用户 在本地注册一个用户然后指向我们的地址然后我们就可以发布包了
    npm adduser --registry http://xxx:4873
    Username: xxx
    Password: xxx
    Password:  xxx
    Email: (this IS public) xxx 
    Logged in as yg-ui on http://xxx/ (你的ip地址)
    这时候我们就注册一个用户,我们可以用这个用户名和密码去登录去上图窗口去登录了

    下次我们再登录时,只需要输入

    npm login

    然后依次输入账户密码,接着检查当前用户。

    npm who am i

    查看当前用户用户是否登录成功

    四、发布依赖包

    首先必须设置镜像源:

    npm set registry http://localhost:4873

    为了方便管理npm源,推荐安装 nrm, 具体如何使用这里不多介绍可以直接百度。
    接着,以vuecli3库模式为例子,发布vue组件到私服
    1、 package.js 中新增一条编译为库的命令

    "lib": "vue-cli-service build --target lib --name vcolorpicker --dest lib packages/index.js"
    • --target : 构建目标,默认为应用模式。这里修改为 lib 启用库模式。
    • --dest : 输出目录,默认 dist 。这里我们改成 lib
    • [entry] : 最后一个参数为入口文件,默认为 src/App.vue 。这里我们指定编译 packages/ 组件库目录。

      在库模式中,Vue是外置的,这意味着即使在代码中引入了 Vue,打包后的文件也是不包含Vue的。
      如果我们在依赖中引入了 lodash 等生产环境必须安装的依赖库等,我们可以在vue.config.js中配置 external 属性, 使之不打包对应的依赖包。

       module.exports = {
          configureWebpack:{
              externals: {
                 'vue': 'Vue',
                 'vue-router':'VueRouter',
                 'axios': 'axios',
                 'lodash' : '\_'
              }
          }
       }

    2、 配置 package.json

      {
        "name": "packagename",
        "version": "0.1.5",
        "description": "基于 Vue 组件库",
        "main": "lib/index.js",
        "keyword": "vcolorpicker colorpicker color-picker",
        "files": ['dist', 'page'],
        "private": false
      }

    其中

    • private : 必须设置为 fasle
    • files : 设置要上传到npm库的文件目录
    • main : 项目入口,默认为同级目录的 index.js
    • name : npm包名,就是我们 import xxx from packagename 中的包名

    也可以这么理解 import xxx from packagenamexxx 就是引入 main 入口中 lib/index.js暴露出的变量名。

    3、 添加.npmignore 文件,设置忽略发布文件
    package.jsonfiles 属性一样,我们发布到 npm 中,只有编译后的 libpackage.jsonREADME.md等文件才是需要被发布的。所以我们需要设置忽略目录和文件。
    语法同.gitignore 的语法一样,具体需要提交什么文件,看各自的实际情况。

    # 忽略目录
    examples/
    packages/
    public/
     
    # 忽略指定文件
    vue.config.js
    babel.config.js
    *.map

    4、登录到 npm

    npm login

    具体登录过程同上,具体不多说了。

    5、 发布到 npm

    npm publish

    执行后显示发布成功即可在npm私服上找到自己的包,如果没有发布成功,有可能是包名称或者版本重复了,改个配置即可。

    五、 内网npm私服搭建

    将以下对应的外网目录拷贝到内网环境中

    文件:C:\\Users\\用户名\\AppData\\Roaming\\npm\\verdaccio

    文件:C:\\Users\\用户名\\AppData\\Roaming\\npm\\verdaccio.cmd

    目录:C:\\Users\\用户名\\AppData\\Roaming\\npm\\node\_modules\\verdaccio

    文件:C:\\Users\\用户名\\AppData\\Roaming\\verdaccio\\config.yaml

    目录:C:\\Users\\用户名\\AppData\\Roaming\\verdaccio\\storage

    注意,其中 storage 目录是存放npm依赖包的地方, 我们可以先直接在外网发布好npm包,然后把storage文件夹复制到内网,接着打开内网verdaccio地址,就能发现这些依赖包自动发布到内网了

    我们发布依赖包到npm私服,有两种包,一种是自己开发的包,另一种是外网npm上的开源包,如果想要将开源包发布到自己的私服上使用,请注意一下几点(踩着坑过来的!!!):

    • 1: 要发布的npm依赖包,最好用npm下载,别用cnpm,因为cnpm包含了各种快捷方式,拷贝到其他电脑会出现各种问题
    • 2: 注意查看发布的npm包中package.json中的 script 中是否含有 republish等钩子属性,republish 会在发布之前执行该命令,而往往我们并不具备执行该命令的环境,从而报错,我们可以将它删除
    • 3:依赖包中有可能嵌套了其他依赖包,该依赖包中子目录的 node_modules文件夹查看依赖情况,发布之前也必须一并将它发布。因为 node_modules 是冒泡向上查找依赖的,相同依赖包有不同版本,公共版本被提取到了依赖包的最外层,特殊版本只能存在于当前目录底下 node_modules文件夹
    • 4:如果想发布指定 package.json 下的所以npm包,正常情况下至少有100多个依赖包,我们手动发布是很辛苦的。但是, verdaccio又不支持一键导入,所以我们只能一个个发布。这里我写了一个 node自动读取 node_modules 目录下的文件并且自动发布依赖包到 npmpublish 的文件,供大家试用!
    const fs = require('fs');
    const path = require('path');
    const process = require('process');
    // 导入执行控制台命令的方法
    const exec = require('child_process').execSync;
    // 保存根路径
    const rootPath = __dirname;
    let result = fs.readdirSync(rootPath);
    const consoleLogPath = path.join(rootPath, 'log');
    result.forEach(fName => {
        const dPath = path.join(__dirname, fName);
        const stat = fs.statSync(dPath);
        if (stat.isDirectory()) {
            const modulePath = path.join(dPath, 'node_modules');
            if (!fs.existsSync(modulePath)) return;
            const statModules = fs.statSync(modulePath);
            if (statModules.isDirectory()) {
                const res = fs.readdirSync(modulePath)
                getVersion(res, consoleLogPath, modulePath, fName)
            }
        }
    })
    
    function getVersion(result, logPath, modulePath, fName) {
        result.forEach(item => {
            // 拼接当前包路径
            const dPath = path.join(modulePath, item);
            // 获取路径的stat
            const stat = fs.statSync(dPath);
            // 判断该包路径是否为目录
            if (stat.isDirectory()) {
                // 如果是目录,则执行下述操作
                // 当前包目录下的package.json文件路径
                const packageJsonPath = path.join(dPath, 'package.json');
                if (fs.existsSync(packageJsonPath)) {
                    // 读取当前包目录下的package.json文件的内容,返回字符串
                    const packageJsonContentString = fs.readFileSync(packageJsonPath, 'utf8');
                    // 解析package.json文件的内容成json格式的对象
                    const parsedPackJson = JSON.parse(packageJsonContentString);
                    if (parsedPackJson.scripts && Object.keys(parsedPackJson.scripts).length !== 0) {
                        parsedPackJson.scripts = {};
                        fs.writeFileSync(packageJsonPath, JSON.stringify(parsedPackJson));
                    }
                    // 把当前的目录切换到当前包路径下
                    process.chdir(dPath);
                    // 在包路径下执行cmd控制台命令 npm publish
                    try {
                        exec('npm publish');
                    } catch {
                        console.log(`包${fName}:${item}已发布`);
                    }
                    // 操作执行完成,则把当前路径切换到原来的根目录
                    process.chdir(rootPath);
                    console.log(fName, parsedPackJson.name, parsedPackJson.version)
                    // 如果解析好的json格式的对象存在scripts属性且该属性不为空对象,那么将其内容设置成空对象
                    fs.appendFileSync(logPath, `${fName} / ${parsedPackJson.name} /@${parsedPackJson.version}\n`);
                }
            }
        })
    }

    参考文档

    1. 《npm 模块安装机制简介》
    2. 《node_modules 困境》
    查看原文

    赞 6 收藏 5 评论 1

    yaoxfly 赞了文章 · 5月7日

    JS常见简单算法排序

    我们面试中经常会遇到排序算法问题,我整理了冒泡排序、选择排序、插入插排等常见简单排序方法。希望此文想对了解排序的前端同学有所帮助。

    封装排序数组

    为了简单高效演示算法的实现思路,我先封装一个构造函数。以下排序我们默认都是从小到大排序,因为不论从大到小或者从小到大思路都一样。

    function ArrayList() {
        // 创建排序数组
        this.list = [6, 3, 8, 5, 4, 1, 9, 7];
        // 排序结果以 - 分割展示
        this.toString = function () {
          return this.list.join('-')
        }
        // 交换数组 m 和 n 的位置
        this.swap = function (m, n) {
          var temp = this.list[n];
          this.list[n] = this.list[m];
          this.list[m] = temp;
        }
      }
    

    冒泡排序

    冒泡排序是依次比较相邻的两个数,将比较小的数放在前面,比较大的数放在后面。

      ArrayList.prototype.bubblesort = function () {
        // 第一个元素和第二个元素互换,大的放在后面, 9放最后面 [6, 3, 8, 5, 4, 1, 9, 7] => [3, 6, 5, 4, 1, 8, 7, 9]
        // 除了最后一个元素9,接着比较大小互换位置  [3, 6, 5, 4, 1, 8, 7, 9] =>  [3, 5, 4, 1, 6, 7, 8, 9]
        // 除了最后一个元素9和倒二个元素8,接着比较大小互换位置 [3, 5, 4, 1, 6, 7, 8, 9] => [3, 4, 1, 5, 6, 7, 8, 9]
        // 以此类推...   [3, 4, 1, 5, 6, 7, 8, 9] =>  [3, 1, 4, 5, 6, 7, 8, 9]
        var len = this.list.length;
        for (var j = len - 1; j >= 0; j--) {
          for (var i = 0; i < j; i++) {
            if (this.list[i] > this.list[i + 1]) {
              this.swap(i, i + 1);
            }
          }
        }
        return this.toString(); // 1-3-4-5-6-7-8-9
      }
    

    冒泡排序的比较次数是 1 + 2 +3 + ... + N - 1 = N(N - 1)/2,大O表示法为O(N^2)。如果每两次比较交换一次那么冒泡排序的交换次数为 N(N - 1)/4。

    选择排序

    首先在未排序序列中找到最小元素,存放到排序序列的起始位置,再从剩余未排序元素中继续寻找最小元素,然后放到已排序序列的末尾。

      ArrayList.prototype.selectsort = function () {
        // 遍历一轮找到最小元素1,他的下标为5,下标0和下标5对换位置  [6, 3, 8, 5, 4, 1, 9, 7] =>  [1, 3, 8, 5, 4, 6, 9, 7]
        // 再从第二个元素开始遍历,找到最小值,互换位置   [1, 3, 8, 5, 4, 6, 9, 7] =>  [1, 3, 8, 5, 4, 6, 9, 7]
        // 以此类推...   [1, 3, 8, 5, 4, 6, 9, 7] =>  [1, 3, 4, 5, 8, 6, 9, 7]
        var len = this.list.length;
        for (var j = 0; j < len - 1; j++) {
          var min = j;
          for (var i = j; i < len - 1; i++) {
            if (this.list[min] > this.list[i + 1]) {
              min = i + 1;
            }
          }
          this.swap(min, j);
        }
        return this.toString(); // 1-3-4-5-6-7-8-9
      }
    

    选择排序的比较次数是 N - 1 + N - 2 + N - 3 + ... + 1 = N*(N - 1)/2, 大O表示法为O(N^2)。 但是交换次数为 N - 1,比冒泡排序少很多。

    插入排序

    插入排序将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当是未排序序列。从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。

     ArrayList.prototype.insertsort = function () {
        // 获取第二个元素3,和第一个元素6对比,3比6小,交换位置  [6, 3, 8, 5, 4, 1, 9, 7] => [3, 6, 8, 5, 4, 1, 9, 7]
        // 获取第三个元素8,8 比前面两个元素 6 和 3 都大 所有位置不变 [3, 6, 8, 5, 4, 1, 9, 7] => [3, 6, 8, 5, 4, 1, 9, 7]
        // 以此类推...  [3, 6, 8, 5, 4, 1, 9, 7] => [3, 5, 6, 8, 4, 1, 9, 7]
        var len = this.list.length;
        for (var i = 1; i < len; ++i) {
          var j = i, temp = this.list[j];
          while (this.list[j - 1] > temp && j > 0) {
            this.list[j] = this.list[j - 1];
            j--;
          }
          this.list[j] = temp;
        }
        return this.toString();
      }
      var list = new ArrayList(); // 1-3-4-5-6-7-8-9

    插入排序的比较次数是 (1 + 2 +3 + ...+ N - 1)/2 = N*(N - 1)/4。它的比较次数和冒泡排序和选择排序相比是最少的。不过用大O表示法也为O(N^2)。

    以上三种只是简单排序,而插入排序思想是高级排序实现的基础,后期更新介绍希尔排序和快速排序等高级排序内容。

    查看原文

    赞 15 收藏 9 评论 1

    yaoxfly 发布了文章 · 5月7日

    iview按需引入,ie11不兼容,报无效字符问题解决

    准备工作

    //借助插件
    npm install babel-plugin-import --save-dev
    
    // .babelrc
    {
      "plugins": [["import", {
        "libraryName": "view-design",
        "libraryDirectory": "src/components"
      }]]
    }

    在main.js中引入

    import "view-design/dist/styles/iview.css";
    import { Button, Table } from "view-design";
    const viewDesign = {
     Button: Button,
     Table: Table
    };
    Object.keys(viewDesign).forEach(element => {
     Vue.component(element, viewDesign[element]);
    });

    先用google浏览器打开正常,以上操作猛如虎,IE浏览器打开250,好了不废话,下面是解决方案

    解决方案

     //vue.config.js中配置
     chainWebpack: config => {
        //解决iview 按需引入babel转换问题
       config.module
          .rule("view-design")  //  我目前用的是新版本的iview ,旧版本的iview,用iview代替view-design
          .test(/view-design.src.*?js$/)
          .use("babel")
          .loader("babel-loader")
          .end();
     }

    问题原因

    为什么会有如上问题呢? 这个就和babel转换问题有关了,按需引入时,那些组件里js文件未进行babel转换或转换不彻底就被引入了,ie11对es6+的语法支持是很差的,所以以上方法就是让引入文件前就对view-design的src下的所有js文件进行babel转换,举一反三,当按需引入第三方框架时出现这个问题,都可用这方法解决了,只要把规则和正则中view-design进行替换。

    延伸扩展

     //全局引入
     import ViewUI from "view-design";
     Vue.use(ViewUI);
     import "view-design/dist/styles/iview.css";
    tips:在全局引入时,一定要记住不要在.babelrc文件里配置按需导入,会导致冲突
    查看原文

    赞 10 收藏 5 评论 2

    yaoxfly 赞了文章 · 5月7日

    JavaScript+Canvas实现自定义画板

    最近研究了HTML5一些新的元素属性,发现确实好用,特别是里面的Canvas这个新的标签元素。官方介绍:Canvas API(画布)是在HTML5中新增的标签用于在网页实时生成图像,并且可以操作图像内容,基本上它是一个可以用JavaScript操作的位图(bitmap)。以下使用JavaScript结合Canvas实现一个画板功能

    效果演示图:

    代码部分(直接复制便可使用)

    <!DOCTYPE HTML>  
    <html>  
    <head>  
    <meta charset="utf-8" >  
    <title>JavaScript+Canvas实现自定义画板</title>  
    </head>  
    <body>  
    <canvas id="canvas"  width="600" height="300"></canvas>  
    <script type="text/javascript">  
       var canvas = document.getElementById("canvas");  
       var ctx = canvas.getContext("2d");  
       //画一个黑色矩形  
       ctx.fillStyle="black";  
       ctx.fillRect(0,0,600,300);  
       //按下标记  
       var onoff = false;  
       //变量oldx跟oldy代表鼠标移动前的坐标  
       var oldx = -10;  
       var oldy = -10;  
       //设置颜色  
       var linecolor = "white";  
       //设置线宽  
       var linw = 4;  
       //添加鼠标移动事件  
       canvas.addEventListener("mousemove",draw,true);  //第三个参数主要跟捕获或者冒泡有关  
       //添加鼠标按下事件  
       canvas.addEventListener("mousedown",down,false);  
       //添加鼠标弹起事件  
       canvas.addEventListener("mouseup",up,false);  
       function down(event){  
         onoff = true;  
         oldx = event.pageX-10;   
         oldy = event.pageY-10;  
      //console.log(event.pageX+'..............000.............'+event.pageY);  
    //event.pageX跟event.pageY相对于整个页面鼠标的位置 包括溢出的部分(就是滚动条)  
      
       }  
      
       function up(){  
         onoff = false;  
       }  
      
       function draw(event){  
         if(onoff == true){   
           var newx = event.pageX-10;   
           var newy = event.pageY-10;   
           ctx.beginPath();//beginPath() 丢弃任何当前定义的路径并且开始一条新的路径。它把当前的点设置为 (0,0)。  
           ctx.moveTo(oldx,oldy);   //移动到点击时候的坐标,以那个坐标为原点  
           ctx.lineTo(newx,newy);   //绘制新的路径  
           ctx.strokeStyle=linecolor;  
           ctx.lineWidth=linw;  
           ctx.lineCap="round";  
           ctx.stroke();//stroke() 方法会实际地绘制出通过 moveTo() 和 lineTo() 方法定义的路径。默认颜色是黑色。  
        //将新的鼠标位置赋给下一次开始绘制的起始坐标  
           oldx = newx;  
           oldy = newy;  
        };  
     };  
    </script>  
    </body>  
    </html>

    最后

    觉得文章不错的,给我点个赞哇,关注一下呗!
    技术交流可关注微信公众号【GitWeb】,加我好友一起探讨
    微信交流群:加好友(备注思否)邀你入群,抱团学习共进步

    查看原文

    赞 27 收藏 11 评论 1

    yaoxfly 发布了文章 · 4月7日

    手摸手带你走近git

    前言

      本文主要介绍git 密钥生成与配置,基础命令包括cloneaddcommitpullpush等,冲突处理,查看并回退历史版本,以及提交规范等。

    密钥生成与配置

    • 配置用户名和邮箱
    //打开git bash 输入以下命令
    
    //用户名,填你的名字全称 如:yaoxfly 方便识别
    git config --global  user.name 'yaoxfly'
    //邮箱,填你的常用邮箱,代码出错时会发邮件通知你
    git config --global  user.email  '123@qq.com'
    • 生成私钥和公钥
    ssh-keygen -t rsa 并按回车3下
    tips:为什么按三下,是因为有提示你是否需要设置密码,如果设置了每次使用git都会用到密码,一般都是直接不写为空,直接回车就好了.
    • 私钥和公钥文件名和生成路径

      私钥:id_rsa,公钥:id_rsa.pub
    默认文件夹在你打开git bash的目录,或者在你的用户目录~/.ssh文件夹下

    • 配置

    复制id_rsa.pub文件里的所有内容粘贴到需要地方,比如github或码云的公钥配置上

    tips: 在生成key之前 git config --global 姓名和邮箱一定要设置, 否则每次操作都要填写用户名和密码,不要在秘钥上设置密码,直接下一步就好。

    基础

    clone(克融项目)

    git clone  'xxxx'

    add(新增文件)

    git add .  // 匹配所有的文件, 提交被修改的和新建的文件,但不包括被删除的文件 
    git add -u  // update tracked files  更新所有改变的文件,将修改的文件提交到暂存区。 
    git add -A  // 是上面两个功能的合集(git add --all的缩写)

    commit(提交)

    • 提交
    git commit -m  "添加你的注释,一般是一些更改信息"
    • 撤回提交
    //仅仅是撤回commit操作,您写的代码仍然保留
    git reset --soft HEAD^  

    push (远程提交)

    • 提交
    git push origin master:master  
    • fatal: refusing to merge unrelated histories 拒绝合并不相关的历史解决方式,强制提交
    git push origin master:master -f 
    • 提交简写
    git push -u
    tips:出现这个问题的最主要原因还是在于本地仓库和远程仓库实际上是独立的两个仓库。

    pull(拉新合并)

    • git pull

      命令基本上就是 git fetchgit merge 命令的组合体,git 从指定的远程仓库中抓取内容,然后马上尝试将其合并进你所在的分支中。

    • git fetch [remote-name] (拉新)

      这个命令会访问远程仓库,从中拉取所有你还没有的数据。 执行完成后,你将会拥有那个远程仓库中所有分支的引用,可以随时合并或查看。
    但是注意的是 git fetch 并不会自动合并或修改你当前的工作。 你必须手动将其合并入你的工作。

    • git merge [remote-name] (合并)

      合并分支

    • 总结

      使用 git pull,命令来自动的抓取然后合并远程分支到当前分支。(相当于一次执行fetch加merge命令)更简单,更方便快捷,更舒适的工作流程。

    branch(分支)

    • 创建新分支
     git branch newBranch
    • 切换新分支
     git checkout newBranch
    • 合并分支
    git merge newBranch  
    • 删除分支
     git branch -D newBranch
    tips:合并分支前,一定要切换到想要合并的分支上,比如主分支,再执行分支合并操作

    其他命令

    • 查看提交历史
    git log
    • 查看当前状态
    git status
    • 缓存删除
    git rm -r --cached .
    tips:应用场景,比如.gitignore忽略的文件无效的情况等
    • 修改本地代码关联的远程地址
    git remote set-url origin ssh://git@ip:端口/home/git/gitrepo/git.git 
    tips:当代码库远程迁移后,可使用当前功能

    获取指定历史版本源代码

    git clone http://XXXX/XX.git //克融项目
    git log   // 查看commit历史,并找到需要的版本
    git checkout '版本号' //获取
    tips:运行git log命令 ,查看commit后跟着的哈希值就是版本号

    冲突解决

    • 版本分支的问题

        问题描述:提交git仓库时出现:Your branch is up-to-date with 'origin/master'.

    /*解决方案*/
    
    //这时候我们就需要新建一个分支
     git branch newBranch  
    //检查分支是否创建成功,前面的*代表的是当前你所在的工作分支
     git branch
    //切换到你的新分支
     git checkout newBranch
    //添加修改和新增的文件
     git add .
    //提交到本地
     git commit -m "18.03.01"
    //检查是否成功
     git status
    //然后切换到主分支
     git checkout master 
    //将新分支提交的改动合并到主分支上(合并前一定要切换到主分支)
     git merge newBranch   
    // push代码了
     git push -u origin master
    //删除这个分支
     git branch -D newBranch
    • 冲突导致,文件乱码

    ++<<<<<<< HEAD

    ++<<<<<<< new_branch

      可直接删除掉这些乱码,保存后再提交,某些ide可提示,并可删除这些乱码等其他智能操作,如vs code,编码神器,我的最爱,哈哈,强推一波。

    • 提示:仓库内还有未合并的文件,不能提交代码.

       问题描述:committing is not possible because you have unmerged files

    /*解决方案*/
    
    //把你修改的文件一个个添加进去
    git  add '文件名',
    //提交本地
    git commit -a -m  "备注信息" 
    //提交远程
    git push -u

    提交规范

    • 规范说明

    git提交也有规范,业内做的比较好,具有参考价值的就是Angular的提交。

    <type>(<scope>): <subject> #header
    // 空一行
    <body>
    // 空一行
    <footer>
    
    //中文释义
    <类型>[可选的作用域]: <主题> 描述
    
    [可选的正文]
    
    [可选的脚注]
    
    • 主体参数
    参数说明是否必须
    type提交类型true
    scope提交影响的范围false
    subject提交目的的简短描述false
    header内容true
    • type有以下标识

      • feat:增加新功能(feature)
      • fix : 修复bug
      • docs : 文档改变 (documentation)
      • style : 代码格式改变
      • refactor : 某个已有功能重构
      • build : 改变了build工具, 如
      • grunt换成了npm
      • revert: 撤销上一次的 commit
      • perf : 性能优化
      • test : 增加测试
      • ci: 与CI(持续集成服务)有关的改动
      • chore:不修改src或者test的其余修改,例如构建过程或辅助工具的变动
      • del:删除某个内容

    tips:

    1. 如果type为feat和fix,则该 commit 将肯定出现在 Change log 之中。其他特殊情况(docs、chore、style、refactor、test)由你决定,要不要放入 Change log,建议是不要。
    2. 当一次改动包括主要type与特殊type时,统一采用主要type。
    查看原文

    赞 7 收藏 2 评论 2

    认证与成就

    • 获得 80 次点赞
    • 获得 2 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 2 枚铜徽章

    擅长技能
    编辑

    (゚∀゚ )
    暂时没有

    开源项目 & 著作
    编辑

    • yaoxfly-vue2-frame

      基于 vue-cli4 创建的自定义优化改造的框架,集成了 vue2.6.11 、axios/flyio、vuex,router、eslint 等框架自带的和自己封装的一些类库和插件,包括二次封装的 axios/flyio、开发常用的公共 css、公用方法,rem 配置,以及一些常用的配置 gzip、cdn 、keep-alive 等优化。用命令创建新的组件,自动导入公共组件,打包自动提交静态文件到七牛云 cdn 等功能,本框架未添加任何 ui 框架,可自行添加改造 ,pc 端、移动端都可以。

    注册于 2019-12-23
    个人主页被 593 人浏览