Giter Site home page Giter Site logo

blog's People

Contributors

pasoul avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

blog's Issues

由浅入深webpack - 04.02 使用babel-loader编译ES6

babel

安装babel-core babel-loader转义es6语法
npm i -D babel-loader babel-core
创建一个webpack.config.js

module.exports = {
	entry: {
		app: './app.js'
	},
	output: {
		filename: '[name].[hash:5].js'
	},
	module: {
		rules: [
			{
				test: /\.js$/,
				exclude: /node_modules/,
				use: 'babel-loader'
			}
		]
	}
}

test定义一个规则,匹配所有以.js结尾的文件,对这些文件用babel-loader进行es6的转义,exclude表示不需要对node_modules中的第三方库进行转义

babel-preset

虽然定义好了loader,但是babel-loader不知道以什么规范进行转义,这时候需要用到babel的presets,presets其实是对babel规范的汇总,一般有一下几类:

  • es2015/es2016/es2017/latest
    使用es2015也就是我们常说的es6,编译箭头函数、编译const、let、编译class等待,更多细节查看文档
    使用es2016也就是es7,涉及到幂运算符,更多细节可看文档
    使用es2017也就是es8,包含我们经常使用的async/await语法糖,更多细节可看文档
    latest是个特殊的presets,包含了es2015、es2016、es2017一直到目前为止正式发布(不包括stage0-stage3)的规范

  • env
    babel-preset-es20xx和babel-preset-lastest的问题在于它们太过于庞大,即包含了过多在某些情况下不需要的功能,比如大多数浏览器已经支持generator,如果你使用babel-preset-es2015,它仍然会将generator函数编译成复杂的es5代码,这是没有必要的

解决方案:

babel-preset-env和babel-preset-lastest功能相似,但是使用babel-preset-env我们可以声明环境,然后该preset只会编译该环境下缺少的特性的代码
比如我们根据browserslist针对某些浏览器环境进行转义:
比如:

  1. 支持大部分浏览器最新的两个版本以及IE 7+:
"babel": {
"presets": [
    [
      "env",
      {
        "targets": {
          "browsers": ["last 2 versions", "ie >= 7"]
        }
      }
    ]
  ]
},

2.支持超过市场份额5%的浏览器

"targets": {
  "browsers": "> 5%"
}
  1. 某个固定版本的浏览器
"targets": {
  "chrome": 56
}

如何安装: npm install --save-dev babel-preset-env,如果安装的是最新的babel-loader,需要安装最新的preset-env:npm install "babel-loader@^8.0.0-beta" @babel/core @babel/preset-env
更多配置可以参考官方文档
要注意:这也意味着我们需要安装一些插件或preset以处理某些实验特性(stage0-stage3),这些往往不是babel-preset-lastest的一部分
使用babel-preset-env的好处就是你不再需要使用babel-preset-esxxxx

  • babel-preset-react
    与react相关的
  • babel-preset-stage 0 - 3
    实验特性,未正式被发布的

继续对我们的webpack.config.js做修改,增加preset预设

module.exports = {
	entry: {
		app: './app.js'
	},
	output: {
		filename: '[name].[hash:5].js'
	},
	module: {
		rules: [
			{
				test: /\.js$/,
				exclude: /node_modules/,
				use: {
					loader: 'babel-loader',
					options: { // 给这个loader指定参数
						presets: [ // 给babel-preset-env设置参数
							[
								'env',
								{
									targets: {
										browsers: ['>1%', 'last 2 versions']
									}
								}
							]
						]
					}
				}
			}
		]
	}
}

对app.js进行修改,增加一些es6的代码

let func = () => {}
const NUM = 40
let arr = [1, 2, 3]
let arrB = arr.map(item => item * 4)

console.log(arrB)

执行webpack打包,观察打包后生成的文件把es6的代码成功转义成es5:
image

如果把target改成只针对chome浏览器版本为52:

target: {
   chrome: '52'
}

image

babel-plugin

如果给app.js增加一些es6的方法,比如includes和new Set()

let func = () => {}
const NUM = 40
let arr = [1, 2, 3]
let arrB = arr.map(item => item * 4)

console.log(arrB.includes(8))
console.log('new set', new Set(arrB))

打开编译后的文件,发现并没有对includes和set进行转义,而低版本浏览器显然是没有这些方法的,其实babel-preset-env只会针对语法进行转义,而不会对函数和方法(Set、Map、Promise、Generator、Array.from、Object.assign等等)进行转义,因此我们需要babel-polyfill和babel-runtime

  • babel-polyfill
    全局垫片,为应用准备,不适用于框架,相当于对全局的变量进行污染,把没有的方法定义到全局
    安装:npm install --save babel-polyfill

  • babel-runtime
    局部垫片,为开发框架准备,使用es6语法,又不想污染全局变量
    安装:npm install --save babel-plugin-transform-runtime npm install --save babel-runtime

解决babel-preset-env无法转义部分es6的函数和方法:

  1. 应用级

app.js引入安装的babel-polyfill:

import 'babel-polyfill'
let func = () => {}
const NUM = 40
let arr = [1, 2, 3]
let arrB = arr.map(item => item * 4)

console.log(arrB.includes(8))
console.log(arrB)
console.log('new set', new Set(arrB))

发现打包后的文件增大了到了265kb,因为引入了全局垫片
image
同时babel-polyfill给全局定义了includes方法和set函数
image
image
image

2.非应用级:如开发框架
修改webpack.config.js配置,新建.babelrc文件,把options的配置放在.babelrc中

module.exports = {
	entry: {
		app: './app.js'
	},
	output: {
		filename: '[name].[hash:5].js'
	},
	module: {
		rules: [
			{
				test: /\.js$/,
				exclude: /node_modules/,
				use: {
					loader: 'babel-loader',
				}
			}
		]
	}
}

.babelrc:

{
	"presets": [ 
		[
			"env",
			{
				"targets": {
					"browsers": ['>1%', 'last 2 versions']
				}
			}
		]
	],
	plugins: ["transform-runtime"]
}

观察打包后的文件,发现run-time仅仅是对es6的方法进行了修改,没有增加全局变量
image
image
同时文件也仅仅增加到了40+kb
image

Linux安装配置jenkins-docker篇

安装与运行

系统信息

[lukou@deploy ~]$ lsb_release -a
LSB Version:    :core-4.1-amd64:core-4.1-noarch
Distributor ID: CentOS
Description:    CentOS Linux release 8.2.2004 (Core)
Release:    8.2.2004
Codename:   Core

下载镜像

docker pull jenkins/jenkins

查看镜像

docker images

后台运行jenkins

docker run --name jenkins -u root -d -v /var/jenkins_home:/var/jenkins_home -p 8080:8080 -p 50000:50000 --restart=always jenkins/jenkins
参数说明:
-name jenkins 容器名称为jenkins
-d 后台运行
-v /var/jenkins_home:/var/jenkins_home 将容器内Jenkins的配置映射到宿主机上,容器删除仍可保留配置,-v参数中,冒号":"前面的目录是宿主机目录,后面的目录是容器内目录,
-p 端口映射
-u root 以root身份运行

查看docker运行的镜像

docker ps

替换插件安装源

sed -i 's/http:\/\/updates.jenkins-ci.org\/download/https:\/\/mirrors.tuna.tsinghua.edu.cn\/jenkins/g' /var/jenkins_home/updates/default.json
sed -i 's/http:\/\/www.google.com/https:\/\/www.baidu.com/g' /var/jenkins_home/updates/default.json

浏览器打开Jenkins

image

查看密码

# 如果没有指定-v,需要进入容器查看密码
# docker exec -it jenkins /bin/bash
cat /var/jenkins_home/secrets/initialAdminPassword

安装插件

image

设置账户密码

接下来就是设置账户密码
image

配置jenkins

安装插件

image
• Git Parameter:构建时可以选择分支
• NodeJS:构建时执行npm script
• Publish over SSH:用ssh发送构建产物至目标服务器

安装Nodejs

在jenkins镜像里面安装node

  1. 安装curl
    apt-get install curl
    如果启动jenkins镜像时没添加-u root参数,则这一步会报错:
jenkins@b138034161de:/$ apt-get install curl
E: Could not open lock file /var/lib/dpkg/lock - open (13: Permission denied)
E: Unable to lock the administration directory (/var/lib/dpkg/), are you root?

root身份登录后可以正常安装

root@cb0c931a18d2:/# apt-get install curl
Reading package lists... Done
Building dependency tree
Reading state information... Done
curl is already the newest version (7.52.1-5+deb9u10).
  1. 安装nvm
    curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.34.0/install.sh | bash
    安装完了根据提示在终端执行如下命令即可使用
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"  # This loads nvm
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"  # This loads nvm bash_completion
  1. 安装node
    nvm install v12.9.1
    #默认使用12.9.1版本node
    nvm alias default v12.9.1
  2. 如果报错安装失败
WARNING: failed to autodetect C++ compiler version (CXX=g++)
WARNING: failed to autodetect C compiler version (CC=gcc)
ERROR: No acceptable C compiler found!
       Please make sure you have a C compiler installed on your system and/or
       consider adjusting the CC environment variable if you installed
       it in a non-standard prefix.
nvm: install v12.9.1 failed!

首先检查gcc(gcc --version)或g++(g++ --version)是否安装
如果显示:gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.4)
证明环境已安装gcc 但版本过低所以没办法完成编译会报错
重新下载或更新就可以了,重新下载需先卸载:
apt-get remove gcc
若没安装:

apt-get install gcc
apt-get install g++

如果安装gcc报错:

root@cb0c931a18d2:~/.ssh# apt-get install gcc
Reading package lists... Done
Building dependency tree
Reading state information... Done
E: Unable to locate package gcc

需要先执行:
apt-get update
安装zip(可选)
(最快方法)查看宿主机的zip位置

whereis zip
zip: /usr/bin/zip /usr/share/man/man1/zip.1.gz

拷贝到容器中:
sudo docker cp /usr/bin/zip jenkins:/usr/bin/zip
如果需要发送压缩包,则需要安装zip:
apt-get install zip
如果报错:

root@19009acbf6f2:~# apt-get install zip
Reading package lists... Done
Building dependency tree
Reading state information... Done
Package zip is not available, but is referred to by another package.
This may mean that the package is missing, has been obsoleted, or
is only available from another source
E: Package 'zip' has no installation candidate

解决方案:

apt-get -y update
apt-get install zip

Docker命令

  1. docker images: 查看镜像
  2. docker ps: 查看运行的容器
  3. docker ps -a: 查看所有容器
  4. docker exec -it { containerId } /bin/bash: 进入某个容器的命令行
  5. docker stop { containerId }: 停止某个容器
  6. docker rm { containerId }: 移除某个容器
  7. docker rmi { imageId }: 移除某个镜像
  8. docker build -t { imageName } .: 生成一个镜像
  9. docker run -p {容器端口:宿主端口} -d { imageName }: 启动某个容器

typescript手册-类与接口

之前我们总结对象的类型-接口时,了解到接口有两个作用,一个是对对象的形状进行描述,而这一节讲述另一个作用:对类的一部分行为进行抽象

类实现接口

一般来讲,一个类只能继承自另外一个类,有时候不同的类有相同的特性,可以把这些特性单独提取成一个接口(interface),然后用类去实现(implements)它。

比如门Door是一个类,防盗门是门的子类,防盗门有一个报警的方法,这时另一个类Car,也有报警的功能,我们可以把报警这个功能单独提取成一个接口,让防盗门和车都去实现它:

interface Alarm {
  alert();
}
class Door{

}
class SecurityDoor extends Door implements Alarm {
  alert() {
    console.log('SecurityDoor alert');
  }
}
class Car implements Alarm {
  alert() {
    console.log('Car alert');
  }
}

如果Car没有实现接口定义的alert方法,编译将会报错:

class Car implements Alarm {
 
}
 // Class 'Car' incorrectly implements interface 'Alarm'.
 //  Property 'alert' is missing in type 'Car'.

一个类可以实现多个接口:

interface Alarm {
  alert();
}
interface Light {
  lightOn();
  lightOff();
}
class Car implements Alarm,Light {
  alert() {
    console.log('Car alert');
  }
  lightOn(){
    console.log('open light')
  }
  lightOff(){
    console.log('close light')
  }
}

typescript手册-类型推导

类型推导

如果没有明确指定类型,typescript会根据类型推导的规则推断出一个类型。

简单的例子

我们声明了一个变量x,并初始化赋值为1,注意我们没有给变量x指定类型,然后将x重新赋值为字符串'1',此时编译将会报错:

let x = 1;
x = '1';
// Type '"1"' is not assignable to type 'number'.

问题的原因就是在没有指定类型的时候,typescript会推断出x的类型是number类型,上面的例子等价于:

let x:number = 1;
x = '1';

如果定义的时候没有赋值,将会被推断成any类型,而完全不会被类型检查

let x;
x = 1;
x = '1';
x = true;

参考:

重学js-类型、值和变量

概述:

JavaScript的数据类型分为两类:原始类型和对象类型。JavaScript的原始类型包括:数字、字符串、布尔值、nullundefinedJavaScript除了原始类型数据以外的都是对象,对象(object)是属性(property)的集合,每个属性都由键值对构成。

普通的JavaScript对象是键值对的无序集合,JavaScript同样定义了一种特殊对象--数组(array),表示带编号的值的有序集合。JavaScript专门为数组定义了语法,使其具有与普通的对象不同的行为特性。

JavaScript还定义了另一种特殊的对象—函数(function),函数是具有与它相关联的可执行代码的对象,通过调用函数来运行可执行的代码,并返回运算结果。和数组一样,函数的行为特征与其他对象都不一样,JavaScript为函数定义了专用语法。对于JavaScript函数来讲,最重要的是,它们都是真值,并且JavaScript可以将他们当做普通对象来对待。(常见假值:0, null, undefined, ‘’, NaN, false)。

如果函数用来初始化(使用new运算符)一类对象,我们称之为构造函数(constructor)。每个构造函数定义了一类对象--由构造函数初始化的对象组成的集合,类可以看成对象类型的子类型。除了数组(Array)类和函数(Function)类以外,JavaScript语言核心定义了其他三种有用的类,分别是:

  • 日期(Date)类定义了代表日期的对象
  • 正则(RegExp)类定义了表示正则表达式的对象
  • 错误(Error)类定义了那些在JavaScript程序中运行时错误和语法错误的对象

JavaScript的类型也可以分为拥有方法的类型和不用有方法的类型(只有nullundefined)。
同样也可以分为可变类型(mutable)和不可变类型(immutable),对象和数组属于可变类型,比如修改对象属性值和数组某个元素的值。数字、布尔、字符串、nullundefined是不可变类型,比如你没办法修改某个数字的内容,即使是字符串,你可以把它看成字符组成的数组,可以访问字符串任意位置的文本,但是无法修改字符串的内容。

JavaScript可以自由的进行数据类型转换,比如,如果程序希望在使用字符串的地方使用了数字,JavaScript会自动的将数字转换成字符串。如果在期望使用布尔值的地方使用了非布尔值,JavaScript也会进行相应的转换。JavaScript灵活的类型转换规则,对判断相等的定义也有影响。等号运算符“==”所进行的类型转换在后面会提到。

JavaScript变量是无类型的(untyped),变量可以被赋为任意类型的值。使用var关键字声明(declare)变量。JavaScript采用词法作用域,不在任何函数内声明的变量称为全局变量(global variable),它在JavaScript程序中的任何地方都是可见的。在函数内声明的变量具有函数作用域(function scope),并且只在函数内可见。

typescript手册-数据类型

typescript支持几乎与javascript相同的基础数据类型:NumberStringBooleanUndefinedNull,在此基础上,typescript还扩展了voidany等常见类型数据。

基础数据类型声明

// 声明一个number类型变量
let n:number = 2;

typescript,如果未按照类型注解给变量赋值,比如将字符串赋值给n,将会在编译阶段报错:
Type '"1"' is not assignable to type 'number'.

// 声明一个布尔类型的变量
let hasMore:boolean = true;
// 声明一个string类型变量
let nickname:string = "zhangsan";
// null和undefined
let un:undefined = undefined;
let nu:null = null;
// 默认情况下null和undefined是所有类型的子类型。 就是说你可以把null和undefined赋值给number类型的变量
let n:number = undefined;

然而,当你设置了--strictNullChecks表示严格校验空值,undefinednull只能赋值给它们各自或者void类型。因为未避免发生不可预知的错误,鼓励使用--strictNullChecks

扩展数据类型

任意值

有时候我们声明一个变量的时候不确定它是什么类型,它有可能是动态的内容,比如用户输入的值,这时我们希望编译器可以直接通过对它们的检查。

// 声明一个任意值类型变量
let notSure:any = "123";
notSure = 123;
notSure = true;

任意值类型在声明数组的时候也非常有用,比如数组的元素的值是任意类型:

let arr:any = [1, '1', true]

空值

通常,我们用空值void声明一个没有返回值的函数,很少用它声明一个变量,因为意义不大,只能赋值undefinednull(严格校验空值的时候,只能赋值undefined

// 声明一个空值类型变量
let empty:void = undefined;
empty = null;

// 声明一个没有返回值的函数
function fn():void {
  console.log("no return value");
}
// 等价于
function fn():void {
  console.log("no return value");
  return undefined;
}

typescript手册-类型别名type

类型别名通常用来给一个类型取新名字。本质上不会创建一个新的类型,而是对原有类型的引用。

语法

type 新名字 = 类型

简单的例子

type Name = string;
// NameResolver表示这是一个函数类型,并且函数的返回值类型是string
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver): Name {
    if (typeof n === 'string') {
        return n;
    }
    else {
        return n();
    }
}

利用chrome浏览器调试手机微信网页的UI

安卓:

  1. 开发微信页面,chrome与安卓真机(安卓4.4及以上)联机调试

  2. 手机中打开“设置”->”开发人员选项”->”USB调试” ,注意此时需要手机与电脑通过usb连接

  3. 用Android机在微信端访问 http://debugx5.qq.com

  4. 在打开的网页中选择 【信息】->【TBS settings】,勾选 【是否打开 TBS 内核 Inspector 调试功能】 ,重启微信

  5. 打开pc端chrome, 在地址栏中输入chrome://inspect/#devices 选中discover usb devices,注意首次联机需要翻墙

常见的js面试题

请实现 flatten(input) 函数,input 为一个 javascript 对象(Object 或者 Array),返回值为扁平化后的结果。

/*
 * 说明:请实现 flatten(input) 函数,input 为一个 javascript 对象(Object 或者 Array),返回值为扁平化后的结果。
 * 示例:
 *   var input = {
 *     a: 1,
 *     b: [ 1, 2, { c: true }, [ 3 ] ],
 *     d: { e: 2, f: 3 },
 *     g: null,
 *   }
 *   var output = flatten(input);
 *   output如下
 *   {
 *     "a": 1,
 *     "b[0]": 1,
 *     "b[1]": 2,
 *     "b[2].c": true,
 *     "b[3][0]": 3,
 *     "d.e": 2,
 *     "d.f": 3,
 *     // "g": null,  值为null或者undefined,丢弃
 *  }
 */
var result = {}
function isObject(val) {
  return Object.prototype.toString.call(val) === '[object Object]'
}
function isArray(val) {
  return Array.isArray(val)
}
function flat(input, prefix = "") {
  if (isObject(input)) {
    for (let key in input) {
      let item = input[key];
      if (!isObject(item) && !isArray(item) && item !== null && item !== undefined) {
        prefix ? result[`${prefix}.${key}`] = item : result[`${key}`] = item
      } else if (isObject(item) || isArray(item)) {
        flat(item, prefix ? `${prefix}.${key}` : key)
      }
    }
  } else if (isArray(input)) {
    for (let key in input) {
      let item = input[key];
      if (!isObject(item) && !isArray(item) && item !== null && item !== undefined) {
        prefix ? result[`${prefix}[${key}]`] = item : result[`${key}`] = item
      } else if (isArray(item) || isObject(item)) {
        flat(item, prefix ? `${prefix}[${key}]` : key)
      }
    }
  } else if (input !== null && input !== undefined) {
    result[prefix] = input
  }
}

typescript手册-泛型

泛型是指在定义函数接口的时候,不预先指定具体的类型,而在使用的时候再指定类型

简单的例子

首先,我们来实现一个函数 createArray,它可以创建一个长度为5的数组,同时将每一项都填充一个默认值

function createArray(value: any):Array<any> {
  let result = [];
  for (let i = 0; i < 5; i++) {
    result.push(value)
  }
  return result;
}
console.log(createArray(1)); // [1, 1, 1, 1, 1]

上例中,我们使用了数组泛型来定义返回值的类型

这段代码编译不会报错,但是一个显而易见的缺陷是,它并没有准确的定义返回值的类型,Array<any>表示返回值可以是任意类型,但我们期望的是返回数组的每一项类型都是输入值value的类型。

这时候,泛型就派上用场了:

function createArray<T>(value: T):Array<T> {
  let result: T[] = [];
  for (let i = 0; i < 5; i++) {
    result.push(value)
  }
  return result;
}
console.log(createArray<number>(1));

上例中,我们在函数名后添加了 <T>,其中 T 用来指代任意输入的类型,在后面的输入value: T和输出 Array<T> 中即可使用了。

接着在调用的时候,可以指定它具体的类型为 number

多个类型参数

定义泛型的时候,可以一次定义多个类型参数:

function swap<T, U>(tuple: [T, U]): [U, T] {
  return [tuple[1], tuple[0]];
}
swap<number, string>([7, 'seven']) // ["seven", 7]

上例中,我们定义swap函数,用于交换元组的元素,泛型定义了两个类型,分别对应元组中的元素类型

由浅入深webpack - 02 webpack核心概念

Entry

  • 代码的入口

  • 打包的入口

  • 单个或多个

如何定义一个entry:
01:入口是一个文件

module.exports = {
      entry: 'index.js'
}

02:入口是多个文件

module.exports = {
      entry: ['index.js', 'vendor.js']
}

03:为entry定义一个key,理解成一个独特的chunk(代码块)

module.exports = {
     entry: {
              index: ['index.js', 'app.js'],
              vendor: 'vendor.js'
     }
}

Output

  • 打包成的文件(bundle)

  • 一个或多个

module.exports = {
	entry: {
		index: 'index.js'
	},
	output: {
		filename: 'index.min.js'
	}
}

name为文件入口文件的名称,分别为index和vendor, hash是webpack为文件进行md5处理生成的字符串,5是指定字符串的长度,便于版本控制

module.exports = {
	entry: {
		index: 'index.js',
		vendor: 'vendor.js'
	},
	output: {
		filename: '[name].min.[hash:5].js'
	}
}
  • 自定义规则
    定义publicPath,指定文件引用路径,可以关联cdn

Loaders

webpack可以使用loader对文件进行预处理,这允许你打包除js以外的任何静态资源,比如css-loader,可以把css变成js的模块,在js文件中引入进来

module.exports = {
	module: {
		rules: [
			{
				test: /\.css$/,
				use: 'css-loader'
			}
		]
	}
}

常用的loader

  • 编译相关
    babel-loader、ts-loader

  • 样式相关
    style-loader、css-loader、less-loader、sass-loader、postcss-loader

  • 文件相关
    file-loader、url-loader

Plugins

插件参与到打包的整个过程中,目的在于处理loader无法处理的其他事情,比如

  • 打包优化与压缩
var webpack = require('webpack');
module.exports = {
	plugins: [
		new webpack.optimize.UglifyJsPlugin()
	]
}
  • 配置编译时的变量

常用的plugin

  • 优化相关

    CommonsChunkPlugin:将公共模块拆分成一个独立的文件,会在最开始的时候加载一次,便缓存到浏览器中供后续使用
    UglifyjsWebpackPlugin:压缩、混淆js

  • 功能相关
    ExtractTextWebpackPlugin:从一个或者多个bundle提取文本到独立的文件当中
    HtmlWebpackPlugin:生成html文件
    HotModuleReplacementPlugin:热更新插件
    CopyWebpackPlugin:把项目中已经用到的第三方库移动到打包好的目录中

名词

  • Chunk代码块:webpack会把文件打包成一个个的chunk,CommonsChunkPlugin会把文件提取成单独的chunk,动态懒加载会把文件从bundle中提取出来,打包成chunk

  • Bundle:打包后的文件,可以理解成一个或者多个chunk组成

  • Module:模块,Loader可以把除js以外的文件处理成模块

重学js-数组

前言

数组是值的有序集合。数组每个元素所处的位置以数字表示,称为索引。数组元素索引不一定要连续的,它们之间可以有空缺,每一个JavaScript数组都有一个length属性。针对非稀疏数组,该属性就是数组元素的个数,针对稀疏数组,length比所有元素的索引要大。

JavaScript数组是JavaScript对象的特殊形式,数组的索引实际上碰巧是整数的属性名差不多。通常,数组的实现是经过优化的,用数字索引来访问数组一般来说比访问常规属性的对象要快很多。

数组继承自Array.prototype中的属性,它定义了一套丰富的数组操作方法,大多数这些方法对‘类数组’也有效。

重学js-对象

对象是Js的基本数据类型,对象是一种复合值:它将很多值(原始值或者其他对象)聚合在一起,可通过名字访问这些值。对象也可看做数属性的无序集合,每个属性都是一个名/值对。属性名是字符串,因此我们可以把对象看成是字符串到值的映射。这种基本数据结构有很多叫法:‘字典’、‘散列’、‘散列表’、‘关联数组’等。

然而对象不仅仅是字符串到值的映射,除了可以保持自有的属性,Js对象还可以从一个称为原型的对象继承属性,对象的方法通常是继承的属性,这种“原型式继承”是Js的核心特征。

除了字符串、数字、true、false、null和undefined之外。Js中的值都是对象。

对象是可变的,我们通过引用而非值来操作对象。如果变量x是指向一个对象的引用,那么执行var y = x;,变量y也指向同一个对象的引用,而非这个对象的副本。通过变量y来修改这个对象亦会对x造成印象。

属性包括属性名和属性值,属性名可以是包含空字符串在内的所有字符串,但对象中不能存在两个同名的属性。值可以是任意Js值,或者在ES5中,可以是一个gettersetter函数,除了属性名和属性值之外,每个属性还有一个与之相关的值,称为"属性特性":

  • 可写(writable attribute),表明是否可以设置该属性的值
  • 可枚举(enumerable attribute),表明是否可以通过for/in循环返回该属性
  • 可配置(configurable attribute),表明是否可以删除或修改该属性

在ES5之前,通过代码给对象创建的所有属性都是可写的、可枚举的和可配置的。在ES5中则可以对这些特性加以配置。

除了包含属性之外,每个对象还拥有三个相关的对象特性(object attribute):

  • 对象的原型(prototype)指向另外一个对象,本对象的属性继承自它的原型对象
  • 对象的类(class)是一个标识对象类型的字符串
  • 对象的扩展标记指明了ES5中是否可以向该对象添加新属性

最后,我们用下面这些属于来对三类JS对象和两类属性做区分:

  • 内置对象(native object):是由ECMAScript规范定义的对象或类,例如,数组、函数、日期和正则表达式都是内置对象
  • 宿主对象(host object):由Js解释器所嵌入的宿主环境(比如Web浏览器)定义的。客户端Js中表示网页结构的HTMLElement对象均是宿主对象,既然宿主环境定义的方法可以当成普通的Js函数对象,那么宿主对象也可以当成内置对象
  • 自定义对象(user-defined object):由运行中的Js代码创建的对象
  • 自有属性(own property):是直接在对象中定义的属性
  • 继承属性(inherited property):在对象的原型对象中定义的属性

由浅入深webpack - 04.01 使用webpack打包js

命令行方式 webpack entry output

  1. 创建sum.js,导出一个函数
export default function(a, b) {
	return a + b
}

2.创建app.js引入sum.js,并调用sum导出的函数

 // es6 module方式引入
import sum from './sum.js'
console.log('sum(1, 2)', sum(1, 2))
  1. 在webpack中直接打包
    webpack app.js bundle.js

  2. 创建html文件,引入bundle.js,可以在浏览器控制台看到输出的结果

image

  1. 通过CommonJs规范引入模块
    创建decrease.js,导出一个函数
module.exports = function(a, b) {
	return a - b
}

在app.js通过require引入函数

var decrease = require('./decrease.js');
console.log('decrease(1, 2)', decrease(1, 2))

6、通过AMD方式引入模块
创建muti.js,导出一个函数

define(function(require, factory) {
	return function(a, b) {
		return a * b
	}
})

app.js通过require引入模块

require(['./muti'], function(muti) {
	console.log('muti', muti(1,2))
})

注意:通过AMD引入模块,执行webpack app.js bundle.js时,由于AMD是异步加载的过程,webpack会创建一个异步加载的js文件0.bundle.js
image

image
这个bundle的内容就是异步加载的muti.js文件
image

配置文件 webpack --config webpack.conf.js

默认webpack会读取当前路径所在目录下的webpack.config.js或webpackfile.js,如果想自定义webpack配置文件需要--config命令 + 自定义文件名称
创建一个webpack.conf.js文件,设置以下配置项

module.exports = {
	entry: {
		// 指定入口文件
		app: './app.js'
	},
	output: {
		// 指定输入文件的名称,name对应entry的key
		filename: '[name].[hash:5].js'
	}
}

执行webpack --config webpack.conf.js,同样也会生成一个异步的bundle
image

由浅入深webpack - 03 模块化

模块化开发

js模块化

  • 命名空间
    库名.类别名.方法名
var Util = {}

Util.type = Util.type || {}

Util.type.method = function() {}

比如: Util.cookie.get / Util.storage.save

弊端:需要提前个团队成员约定好各自的命名空间,需要通过命名空间路径的方式才能找到一个方法...

  • commonJs(服务器端)
    一个文件为一个模块,外界无法直接访问,必须通过module.export暴露模块接口,通过require引入模块,同步执行
  • ADM/CMD(浏览器端)
    Async Module Definition
    代表作:require.js
    使用define定义模块,使用require加载模块
    依赖前置,提前执行
define(
	// 模块名,可省略
	'alpha',
	// 依赖
	['require', 'exports', 'beta'],
	// 模块输出
	function(require, exports, beta) {
                 // 通过exports对外提供接口
		// 即使没有使用到其中的一个模块,这个模块也会被提前执行
	}
)

Common Module Definition
一个文件为一个模块
使用define来定义一个模块
使用require来加载一个模块
代表作:sea.js
尽可能懒执行
AMD和CMD最显著的区别就是执行方式不一样,AMD先把所有依赖都前置(执行),可以在任意地方require,cmd在只有用到依赖的地方才会去执行

define(function(require, exports, module) {
	// 通过require引入依赖
	var $ = require('jquery')
	// 通过exports对外提供接口
	exports.doSomething = ...
	// 或者通过module.exports提供整个接口
	module.exports = ...
})
  • ES6 Module
    一个文件一个模块
    export/import

webpack支持

AMD
ES Module(推荐的,3.0版本原生就支持import 和 export)
CommonJs

css模块(css设计模式)

oocss
smacss
...

typescript手册-高级类型-联合类型、交叉类型

联合类型

联合类型表示一个值的类型可以是多个类型中的一种,多个类型之间用竖线(|)分开,比如number|string表示类型既可以是number也可以是string

简单的例子

// 声明一个变量,值类型可以是number类型和string类型
let age: number|string = '10';
age = 10;

但是如果我们给age赋值的类型是其他类型,编译将会报错:

let age: number|string = '10';
age = 10;
age = true;
// Type 'true' is not assignable to type 'string | number'

访问联合类型的属性和方法

我们声明了一个函数,参数类型是联合类型number|string,然后在函数体内部尝试访问参数的length属性,这时编译报错:

function getSometing(arg:number|string) {
  console.log(arg.length);
}
Property 'length' does not exist on type 'string | number'.
 Property 'length' does not exist on type 'number'

typescript无法确定参数arg到底是哪个类型,我们只能访问联合类型中所有类型公有的属性和方法,很明显number类型的参数,没有length属性

如果我们访问numberstring的公有属性,�是没问题的:

function getSometing(arg:number|string) {
  console.log(arg.toString());
}

稍复杂的例子

我们分别声明两个接口:ChickenDuck,它们分别有自己的方法:鸡会飞和生蛋,鸭子会游泳和生蛋
然后定义了doSomething函数,参数是animal,它的类型要么是Chicken要么是Duck

interface Chicken {
  fly();
  layEggs();
}

interface Duck {
  swim();
  layEggs();
}

function doSomething(animal:Chicken|Duck) {
  animal.fly();
}

如果我们调用animalfly方法,编译将会报错,因为typescript不知道animal的类型到底是Chicken还是Duck,如果我们传入的是Duck,很明显Duck是不会飞的(接口没有定义fly

Property 'fly' does not exist on type 'Chicken | Duck'.
  Property 'fly' does not exist on type 'Duck'

但是如果我们调用animallayEggs方法,编译通过,因为不管animal的类型是Chicken还是Duck,它们都拥有生蛋(layEggs)的能力,你可以大胆的调用。

function doSomething(animal:Chicken|Duck) {
  animal.layEggs();
}

参考:

typescript手册-枚举

枚举通常用于取值限定在一定范围的场景,比如一周只有七天,颜色被限定为红绿蓝等。

简单的例子

枚举使用enum关键字来定义

enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat};

枚举成员默认被赋值为从0开始递增的数字,同时也可以从枚举值反向映射到枚举名

enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat};

console.log(Days['Sun']); // 0
console.log(Days['Mon']); // 1
console.log(Days['Tue']); // 2
console.log(Days['0']); // Sun
console.log(Days['1']); // Mon
console.log(Days['2']); // Tue

我们来看编译后的代码,如果让我们实现一个对象的属性和属性值之间的映射,大家应该知道怎么做了

var Days;
(function (Days) {
    Days[Days["Sun"] = 0] = "Sun";
    Days[Days["Mon"] = 1] = "Mon";
    Days[Days["Tue"] = 2] = "Tue";
    Days[Days["Wed"] = 3] = "Wed";
    Days[Days["Thu"] = 4] = "Thu";
    Days[Days["Fri"] = 5] = "Fri";
    Days[Days["Sat"] = 6] = "Sat";
})(Days || (Days = {}));

手动赋值

我们也可以给枚举项手动赋值,typescript只支持枚举值为数字字符串类型

数字字面量

enum Days {Sun = 7, Mon = 2, Tue, Wed, Thu, Fri, Sat};

console.log(Days['Sun']);// 7
console.log(Days['Mon']);// 2
console.log(Days['Tue']);// 3
console.log(Days['Wed']);// 4
console.log(Days['Sat']);// 7
console.log(Days['0']);// undefined
console.log(Days['1']);// undefined
console.log(Days['2']);// Mon
console.log(Days['3']);// Tue
console.log(Days['7']);// Sat

上面的例子中,我们未手动赋值枚举项会接着上一个枚举项的值递增。

我们注意到未赋值的最后一项和手动赋值的第一项冲突了,但是typescript不会察觉到,所以Days['7']的值会被覆盖,编译结果是:

var Days;
(function (Days) {
    Days[Days["Sun"] = 7] = "Sun";
    Days[Days["Mon"] = 2] = "Mon";
    Days[Days["Tue"] = 3] = "Tue";
    Days[Days["Wed"] = 4] = "Wed";
    Days[Days["Thu"] = 5] = "Thu";
    Days[Days["Fri"] = 6] = "Fri";
    Days[Days["Sat"] = 7] = "Sat";
})(Days || (Days = {}));

所以在实际使用中我们应该避免出现覆盖的情况。

我们也可以给枚举项赋值为负数、小数:

// 小数
enum Days {Sun = 7, Mon = 2.5, Tue, Wed, Thu, Fri, Sat};

console.log(Days['Sun']); // 7
console.log(Days['Mon']); // 2.5
console.log(Days['Tue']); // 3.5
console.log(Days['Sat']); // 7.5
// 负数
enum Days {Sun = 7, Mon = -2, Tue, Wed, Thu, Fri, Sat};

console.log(Days['Sun']); // 7
console.log(Days['Mon']); // -2
console.log(Days['Tue']); // -1
console.log(Days['Sat']); // 3

我们可以看到无论是前面手动赋值类型是正整数、小数还是负数,未赋值项的递增步长仍然为1。

字符串字面量

手动给枚举项赋值的类型也可以是string:

enum Days {Sun = 7, Mon = 2, Tue, Wed, Thu, Fri, Sat = 'S'};

console.log(Days['Fri']); // 6
console.log(Days['Sat']); // S

需要注意的是,string类型的枚举项后面不能存在其他枚举项,比如:

enum Days {Sun = 7, Mon = 2, Tue, Wed, Thu, Fri = 'F', Sat};

console.log(Days['Fri']); // F
console.log(Days['Sat']); // undefined
// 编译报错:Enum member must have initializer.

这里typescript提示枚举成员必须具有初始化表达式,意思是说我们必须给Fri = 'F'后面的枚举项也手动赋值。

由浅入深webpack - 04.03 提取公共代码

为什么要提取公共代码

  • 减少代码冗余

  • 提高加载速度

CommonsChunkPlugin

CommonsChunkPlugin是webpack内置的插件,用于建立一个独立的文件,这个文件包括多个入口chunk的公共模块,通过将公共模块拆分出来,在最开始的时候加载一次,便存到缓存中供后续使用。
new webpack.optimize. CommonsChunkPlugin(options)

配置

{
	// chunk的名称,如果传入数组,相当于数组的每一项都调用实例化插件
	name: string, // or
	name: string[],
	// chunk的文件名模板,可以包含跟output.filename相同的占位符
	filename: string,
	// 通常是个>1的整数,表示至少被chunk引入多少次,该模块才会被提取到公共chunk里
	// 如果传入Infinity,则立刻会生成公共chunk,但是里面没有模块
	minChunks: number|Infinity|function,
	// 这是一个元素为chunk名称的数组,插件将会从这些chunks里面提取公共chunk
	// 可见如果minChunks是整数,应当小于chunks的个数
	// 如果缺省,则所有入口chunk都会被选中
	chunks: [],
	// 如果设置为true,也会在entry的子模块中查找共同依赖
	children: boolean,
	// 如果设置为true,也会在entry的所有依赖模块中查找共同依赖
	deepChildren: boolean
}

实例

分别创建subPageA.js和subPageB.js,这两个js文件分别引入moduleA.js
subPageA.js

import './moduleA'
export default 'subPageA'

subPageB.js

import './moduleA'
export default 'subPageB'

moduleA.js
export default 'moduleA'
创建项目的入口文件pageA.js,分别引入subPageA.js和subPageB.js

import './subPageA'
import './subPageB'

export default 'pageA'

整个项目的依赖关系应该是这样
image

创建webpack.config.js

var webpack = require('webpack');
var path = require('path');
module.exports = {
	entry: {
		'pageA': './pageA.js'
	},
	output: {
		path: path.resolve(__dirname, './dist'),
		filename: '[name].bundle.js'
	},
	plugins: [
		new webpack.optimize.CommonsChunkPlugin({
			name: 'common',  // 指定生成的公共chunk名称
			minChunks: 2 // 指定被引入两次以上的代码,将被打包到公共chunk里
		})
	]
}

因为依赖了webpack内置的插件,需要安装webpack,执行
npm i -S webpack
执行webpack打包,生成pageA.bundle.js和common.bundle.js
image
我们希望common.bundle.js包含我们的moduleA.js,因为这个模块同时被subPageA.js和subPageB.js引用,但实际上,moduleA.js依然被打包进pageA.js,而common.bundle.js里面只包含webpack打包的一些代码
image
image

为什么提取失败?

实际上CommonsChunkPlugin只针对多entry有效,比如再创建一个pageB.js,同样对subPageA.js和subPageB.js进行依赖
PageB.js

import './subPageA'
import './subPageB'

export default 'pageB'

此时项目的依赖应该是这样:
image

再给webpack.config.js添加pageB.js的入口

var webpack = require('webpack');
var path = require('path');
module.exports = {
	entry: {
		'pageA': './pageA.js',
		'pageB': './pageB.js'  // 增加入口pageB.js
	},
	output: {
		path: path.resolve(__dirname, './dist'),
		filename: '[name].bundle.js'
	},
	plugins: [
		new webpack.optimize.CommonsChunkPlugin({
			name: 'common', // 指定生成的公共chunk名称
			minChunks: 2
		})
	]
}

我们希望subPageA.js、subPageB.js和moduleA.js都被打包进公共文件,执行webpack,查看结果:
新增了pageB.bundle.js
image
打包后的pageB.bundle.js
image
打包后的pageA.bundle.js
image
重点是common.bundle.js,它成功地把我们希望的文件打包进来
image

如何把我们的第三方依赖库和业务代码单独打包成各自的公共文件?

引入第三方库lodash.js,然后在pageA.js和pageB.js中引用

import './subPageA'
import './subPageB'
import * as _ from 'lodash'

export default 'pageA'
  1. 如何把第三方库和webpack打包后生成的代码,打包到同一个文件中
    在webpack.config.js添加一个vendor
var webpack = require('webpack');
var path = require('path');
module.exports = {
	entry: {
		'pageA': './pageA.js',
		'pageB': './pageB.js',
		'vendor': ['lodash'] // 数组,里面是我们的第三方库
	},
	output: {
		path: path.resolve(__dirname, './dist'),
		filename: '[name].bundle.js'
	},
	plugins: [
		new webpack.optimize.CommonsChunkPlugin({
			name: 'vendor', // 传入一个已存在的 chunk 名称而被选择
			minChunks: Infinity, // 传入 `Infinity` 会马上生成 公共chunk,但里面没有模块
		})		
	]
}

此时生成了一个vendor.bundle.js,大小为547kb

里面包含了webpack的生成代码和lodash库
image
image

  1. 如果不想把webpack生成代码和第三方库打包在一起,保持第三方库的纯净,同时又希望webpack生成代码独立出来
    修改webpack.config.js,增加一个插件实例
var webpack = require('webpack');
var path = require('path');
module.exports = {
	entry: {
		'pageA': './pageA.js',
		'pageB': './pageB.js',
		'vendor': ['lodash'] // 数组,里面是我们的第三方库
	},
	output: {
		path: path.resolve(__dirname, './dist'),
		filename: '[name].bundle.js'
	},
	plugins: [
		new webpack.optimize.CommonsChunkPlugin({
			name: 'vendor', // 传入一个已存在的 chunk 名称而被选择
			minChunks: Infinity, // 传入 `Infinity` 会马上生成 公共chunk,但里面没有模块
		}),
		new webpack.optimize.CommonsChunkPlugin({
			name: 'manifest', // 传入一个任意非entry的name
			minChunks: Infinity
		}), 		
	]
}

执行webpack查看打包结果,增加了一个manifest.bundle.js文件,同时vendor.bundle.js大小变成541kb
image
同时vendor.bundle.js只包含了lodash的代码
image
manifest.bundle.js单独的把webpack的生成代码提取出来
image

  1. 但是pageA.js和pageB.js的公共代码没有被提取出来
    image
    image

解决方法:
在webpack.config.js里面再增加一个CommonsChunkPlugin实例,用于提取公共业务代码
var webpack = require('webpack');
var path = require('path');

module.exports = {
	entry: {
		'pageA': './pageA.js',
		'pageB': './pageB.js',
		'vendor': ['lodash'] // 数组,里面是我们的第三方库
	},
	output: {
		path: path.resolve(__dirname, './dist'),
		filename: '[name].bundle.js'
	},
	plugins: [
		new webpack.optimize.CommonsChunkPlugin({
			name: 'common', // 提取业务代码
			minChunks: 2
		}),
		new webpack.optimize.CommonsChunkPlugin({
			name: 'vendor'
			minChunks: Infinity
		}),
		new webpack.optimize.CommonsChunkPlugin({
			name: 'manifest'
			minChunks: Infinity
		}), 		
	]
}

执行webpack打包,发现报错如下:
image
在普通模式下,CommonsChunkPlugin不能作用于非入口文件
需要指定范围

var webpack = require('webpack');
var path = require('path');
module.exports = {
	entry: {
		'pageA': './pageA.js',
		'pageB': './pageB.js',
		'vendor': ['lodash'] // 数组,里面是我们的第三方库
	},
	output: {
		path: path.resolve(__dirname, './dist'),
		filename: '[name].bundle.js'
	},
	plugins: [
		new webpack.optimize.CommonsChunkPlugin({
			name: 'common',
			minChunks: 2,
			chunks: ['pageA', 'pageB']  // 指定从pageA和pageB选择公共代码
		}),
		new webpack.optimize.CommonsChunkPlugin({
			name: ['vendor', 'manifest'], // 合并
			minChunks: Infinity
		})		
	]
}

查看打包后的结果,增加了common.bundle.js
image
里面包含了公共的业务代码
image
打包后的pageA.js和pageB.js公共文件都被提取出来
image

typescript手册-函数的类型

函数的定义方式

函数的定义方式有两种:函数声明函数表达式

// 函数声明
function add(x, y) {
  return x+y;
}
// 函数表达式
let reduce = function(x, y) {
  return x - y;
}

函数的类型

一个函数有输入输出,对函数类型的约束本质上是对输入值和输出值的约束,为上面两个函数添加类型声明

函数声明

函数声明的类型定义较简单:

function add(x:number, y:number):number {
  return x + y;
}

这里我们定义了参数xy的类型为number,返回值类型也为number,除了类型的约束,参数的个数也限制为2个,输入多余或者少于要求的参数,是不被允许的

function add(x:number, y:number):number {
  return x + y;
}
add(1, 2, 3);
// Expected 2 arguments, but got 3
function add(x:number, y:number):number {
  return x + y;
}
add(1);
// Expected 2 arguments, but got 1

函数表达式

我们对上面的函数表达式定义类型:

let reduce = function(x:number, y:number):number {
  return x - y;
}

上面我们仅仅对等号右侧的匿名函数进行类型定义,等号左边的reduce是通过赋值之后类型推导推断出来的,准确的来说叫上下文归类,是类型推导的一种,事实上我们也可以给reduce定义类型:

下面是完整的类型声明:

let reduce: (x:number, y:number) => number = function(x:number, y:number):number {
  return x - y;
}

整个(x:number, y:number) => number是对reduce的类型定义,注意 =>并非是ES6的箭头函数,它表示函数输入输出之间的管道,左侧是函数输入值,右侧是函数输出值

好的习惯

我们多次强调函数是由输入值和输出值组成的,即使函数没有任何返回值,我们也要必须指定返回值类型为void而不能留空。

由浅入深webpack - 01 webpack简介

webpack概述

image
官网:https://webpack.js.org/
中文官网:https://doc.webpack-china.org/
github:https://github.com/webpack/webpack
引用官方的一段话:本质上,webpack是一个现代JavaScript应用程序的静态模块打包器(module bundler)。当webpack处理应用程序时,它会递归的构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或者多个bundle

webpack版本更迭

webpack v1.0.0 --- 2014.2.20

  • 编译、打包

  • HMR(模块热更新)

  • 代码分割

  • 文件处理(loader 、plugin)

webpack v2.2.0 --- 2017.1.18

  • Tree Shaking(项目中引入但是没有使用的代码打包的时候会被删除掉)

  • ES module(import 、export)

  • 动态Import(V1.0的require.ensure)

  • 提供新的官方文档

webpack v3.0.0 --- 2017.6.19

  • Scope Hoisting(作用域提升)

  • Magic Comments(配合动态import使用,指定打包后的chunk名)

webpack v4.0.0 --- 2018.2.25

版本迁移

V1 -> V2
迁移指南中文版:https://doc.webpack-china.org/guides/migrating/
V2 -> V3
webpack开发者基本不会修改api,导致基于旧版本webpack的项目需要进行大规模的迁移,升级webpack之后,基本上使用npm update更新依赖即可

webpack新版本特性投票

https://webpack.js.org/vote/

typescript手册-类

传统javascript中,回忆我们是如何使用类的:使用实例化构造函数,通过原型链实现继承,在es6中,我们终于迎来了class,详细的文档移步ECMAScript 6 入门
typescript不仅实现了es6class,还在此基础上做了一些扩展。

一些名词的概念

  • 类(class):定义了一件事物的抽象特点,包含它的属性和方法
  • 对象(object):通过new生成类的实例
  • 面向对象(OOP)的三大特性:封装、继承、多肽
  • 封装:隐藏对象的属性和实现细节,仅对外暴露接口,外界不需要关心其内部实现细节,就能通过接口访问该对象。
  • 继承:子类继承父类,拥有父类所有的特性
  • 多态:由继承产生不同的相关的类,针对同一个方法可以有不同的响应,产生不同的执行结果,比如CatDog都继承自Animal,可以分别实现了自己的eat方法
  • 存取器:读取属性和给属性赋值时,可以添加额外的操作
  • 修饰符:限定成员的性质,比如public表示公有的属性和方法
  • 抽象类:抽象类不允许实例化,只能作为其他类继承的基类,抽象类的方法只能在子类中被实现
  • 接口:不同类之间公有的属性和方法,可以抽象成一个接口。接口可以被类实现,一个类只能继承另外一个类,但是可以实现多个接口。

网站性能优化--动态文件压缩Gzip

Gzip是什么?

gzip是GNUzip的缩写,它是一个GNU自由软件的文件压缩程序。在web应用中,通过Nginx开启Gzip,传输压缩过后的静态文件,可以节省网络带宽,提高搜索引擎抓取的速度,提升网站浏览速度等。

Gzip工作原理

一张图了解客户端和服务器Gzip通信

image

  1. 浏览器请求url,并在request-header中加入Accept-Encoding:gzip表明浏览器支持Gzip,并告知服务器采用何种压缩方式

  2. 服务器收到浏览器的请求后,判断浏览器是否支持Gzip,如果支持,则返回压缩后的内容,并且在response headers中返回Content-Encoding:gzip,如果不支持,则返回未压缩后的内容。

  3. 如果浏览器接收到压缩过的文件,需要进行解压缩操作

淘宝为例,打开控制台:
请求headers:
image

响应headers:
image

可以看到网站支持Gzip,当开启Gzip后,压缩倍率可以用在线工具监测
image
压缩前的文件181k,压缩后的文件75k,压缩率58%,极大的节省了服务器的网络带宽,这对于访问量巨大的淘宝来讲节约的流量非常可观。

Nginx开启Gzip

        ##
	# Gzip Settings
	##

	gzip on;
	gzip_disable "msie6";

	# gzip_vary on;
	# gzip_proxied any;
	# gzip_comp_level 6;
	# gzip_buffers 16 8k;
	# gzip_http_version 1.1;
	# gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

参数配置:

  • gzip on: 开启Gzip

  • gzip_disable "msie6": IE6对Gzip支持不友好,不给它开启了

  • gzip_vary on: 在header里增加Vary: Accept-Encoding,告诉缓存服务器如何缓存和筛选合适的版本(关于vary的研究单独看这里

  • gzip_proxied any: Nginx开启Gzip的条件,比如expired(如果header头包含Expires信息),no-cache(如果header头包含 Cache-Control: no-cache), any (无条件开启)

  • gzip_comp_level 6: 压缩等级1-10,数字越大压缩的越好,同时也越消耗CPU,一般建议5

  • gzip_buffers:设置gzip压缩时使用的缓冲区的个数以及每个缓冲区的大小

  • gzip_http_version:仅对指定版本的http请求的响应进行压缩,通常取值1.1

  • gzip_types:压缩文件类型,注意:javaScript文件类型application/javascript和text/javascript都加上,否则会出现js没有被压缩的文件,text/html默认被压缩,不需要显式的指定

由浅入深webpack - 04.04 代码分割和懒加载

概念

在实际业务中,某些文件不需要初始化的时候就加载进来,我们需要将这部分代码进行分割,只在合适的时机加载它们,这样既可以节省用户带宽,也可以提高页面载入速度

实现

webpack methods

  1. require.ensure
    参数: []:dependencies依赖,不会执行依赖
    callback: 需要require要执行的依赖
    errorCallback(可选)
    chunkName

2.require.include

ES2015 Loader

微信公众号网页开发之授权、支付、分享功能

微信公众号网页和移动端wap的区别

微信公众号网页开发和移动web开发无本质区别,只是在移动web开发的基础上增加了一些微信提供的扩展能力,如:支付、分享、地理定位等,可以理解为:移动web + 微信能力 = 微信公众号网页开发

什么是JS-SDK

微信JS-SDK:是开发者在网页上通过JavaScript代码调用微信原生功能的工具包,开发者可以用它在网页上实现录制和播放语音、扫一扫、分享等能力

微信的用户机制

为了识别用户,每个用户在每个公众号下都有一个OpenId,如果需要在多公众号、移动应用之间做用户共通,则需前往微信开放平台,将这些公众号和应用绑定到一个开放平台账号下,绑定后,一个用户虽然对多个公众号和应用有多个不同的OpenID,但他对所有这些同一开放平台账号下的公众号和应用,只有一个UnionID,此UnionID是在拉取用户信息(需scope为 snsapi_userinfo)时返回的(后续授权功能会提到)

网页授权

如果用户在微信端访问网页,公众号可以通过微信网页授权来访问用户的基本信息。

网页授权回调域名说明:

  1. 在公众号请求网页授权之前,开发者需要先到公众平台官网中的“开发 - 接口权限 - 网页服务 - 网页帐号 - 网页授权获取用户基本信息”的配置选项中,修改授权回调域名。请注意,这里填写的是域名(是一个字符串),而不是URL,因此请勿加 http:// 等协议头;
    image

  2. 授权回调域名配置规范为全域名,比如需要网页授权的域名为:www.qq.com,配置以后此域名下面的页面http://www.qq.com/music.htmlhttp://www.qq.com/login.html 都可以进行OAuth2.0鉴权。但http://pay.qq.comhttp://music.qq.comhttp://qq.com 无法进行OAuth2.0鉴权

  3. 如果公众号登录授权给了第三方开发者来进行管理,则不必做任何设置,由第三方代替公众号实现网页授权即可

网页授权的两种scope区别:

  1. 以snsapi_base为scope发起的网页授权,是用来获取进入页面的用户的openid的,并且是静默授权并自动跳转到回调页的。用户感知的就是直接进入了回调页(往往是业务页面)
  2. 以snsapi_userinfo为scope发起的网页授权,是用来获取用户的基本信息的。但这种授权需要用户手动同意,并且由于用户同意过,所以无须关注,就可在授权后获取该用户的基本信息。
    关于UnionID机制:
    如果开发者有在多个公众号,或在公众号、移动应用之间统一用户帐号的需求,需要前往微信开放平台(open.weixin.qq.com)绑定公众号,因为同一用户,对同一个微信开放平台下的不同应用(移动应用、网站应用和公众帐号),unionid是相同的,此unionid是在拉取用户信息(需scope为 snsapi_userinfo)时返回的

关于静默授权的场景:

  1. 上面已经提到,对于以snsapi_base为scope的网页授权,就静默授权的,用户无感知;
  2. 对于已关注公众号的用户,如果用户从公众号的会话或者自定义菜单进入本公众号的网页授权页,即使是scope为snsapi_userinfo,也是静默授权,用户无感知。

网页授权的流程

image

前端代码实现

假设我们有这样的需求:用户进入网页通过静默授权拿到用户openId,在指定场景下(如设置用户信息)需要手动授权拿到用户的userInfo

  1. 创建login页面,用于网站的授权及重定向操作,我们可以通过mode指定授权模式,通过redirectUrl指定授权成功后重定向的页面,关键代码如下:
export default function() {
  // mode: base 静默授权  userInfo 手动授权
  const query = useLocation().query
  const mode =  query.mode === 'userInfo'
                  ? 'zuc/wx/authorizeUserInfo'
                  : 'zuc/wx/authorizeBase';
  window.location.replace(
    `${config.h5LoginDomain}${mode}?redirectUrl=${encodeURIComponent(query.fromPath)}`,
  );
  return null;
}
  1. 和后端约定好错误码,在全局响应拦截器里检测到该错误码后跳转到登录页进行授权,关键代码:
request.interceptors.response.use(async response => {
  if (response) {
    const data = await response.clone().json();
    if (  data.code === 30000118 ) {
      // 30000118:您的登录已过期,请重新登录
      history.push(
        `/login?fromPath=${encodeURIComponent(location.href)}`,
      );
    }
  }
  return response;
});
  1. 在设置个人资料时进行手动授权,关键代码
handleClick() {
    history.push(
      `/micro-store/h5/login?fromPath=${encodeURIComponent(location.href)}&mode=userInfo`,
    );
}

注意

  1. 确保公众号已认证,只有认证的公众号才有获取用户信息接口的权限
    image
  2. 授权成功后,服务端会在浏览器埋下cookie,因为一般服务端会把cookie的domain设置成网站的域名,而通常我们本地开发环境是通过ip访问项目的,导致请求的时候,无法带上cookie,可以通过修改本地host解决此问题,如:192.168.30.77 xxx.abc.com,我们本地访问地址由原来的:192.168.30.77:8080/index改成xxx.abc.com:8080/index
  3. 常见的请求库如axiosfetchumi-request请求时默认不带cookie的,需要设置credentials: ‘include’。但此属性要求后端设置的Access-Control-Allow-Origin不能为*,此问题在通过CROS解决跨域问题时尤为常见,报错如下:
    image
    服务端常见的解决方案是获取浏览器的请求头,然后设置到Access-Control-Allow-Origin中,当然为了安全,可以把允许跨域访问的域名放在一个集合里,然后判断浏览器的请求头是不是在这个集合中
    image

由浅入深webpack - 04.05 处理css

loader

webpack允许我们在js模块中importcss文件,这一切归功于强大的loader,可以使你在import或‘加载’模块时预处理文件,编译普通css模块一般使用style-loadercss-loader,后者将解析(resolve) import/require()的css模块,包括处理css中的url()@import,前者将解析后的样式嵌入到style标签中,内联到html的head标签

使用style-loader和css-loader

安装

npm install style-loader css-loader --save-dev

用法

建议将style-loader和css-loader结合使用
项目目录:
image

  • dist 存放打包后的文件

  • src/css 创建极简单的样式
    base.css:

html{
	background: #f00;
}
  • app.js 引入css文件
    import './css/base.css'

  • webpack.config.js

var path = require('path')

module.exports = {
	entry: {
		app: './src/app.js'
	},
	output: {
		path: path.resolve(__dirname, 'dist'),
		filename: '[name].bundle.js'
	},
	module: {
		rules: [
			{
				test: /\.css$/,
				use: ['style-loader', 'css-loader']
			}
		]
	}
}
  • index.html 引入打包后的app.bundle.js
    运行webpack,浏览器中查看html,发现head标签增加了style,里面正是base.css的样式,说明我们在app.js中成功引入了css模块
    image

如何通过link标签的形式引入css样式

修改webpack.config.js

var path = require('path')

module.exports = {
	entry: {
		app: './src/app.js'
	},
	output: {
		path: path.resolve(__dirname, 'dist'),
		filename: '[name].bundle.js',
		publicPath: './dist/' // 为了使资源保持正确的路径,必须设置 webpack 配置中的 output.publicPath 属性,以便生成绝对路径
	},
	module: {
		rules: [
			{
				test: /\.css$/,
				use: [
					{
						loader: 'style-loader/url'
					},
					{
						loader: 'file-loader'
					}
				]
			}
		]
	}
}

执行webpack打包,浏览器中查看html文件,原来style标签的内容通过link方式引入进来
image
同时dist目录生成单独的css文件,但是这种方法比较小众,如果引入多个css模块,会产生多余的http请求
image

微信小程序集成Jenkins

我们为什么要持续集成?

试想以下两个场景:

场景一:你在开发新功能的时候,同事A找你要小程序体验二维码,于是你只能:

git stash // 缓存本地修改
git checkout release // 切换到测试分支
yarn test // 打测试包
... // 预览、切回分支
git stash pop // 还原本地修改

然后整理被打断的代码思路,继续coding,暗自吐槽(二维码是有35min过期时间的,一会又得找我了...)

场景二:小程序到了提测日期,恰巧负责开发的你今天请假了,意味着没人可以为测试同学打包,因此耽误了项目进度。

以上两个场景我们发现一个共同的问题,小程序的体验发布太依赖开发者,因为通常只有开发者熟悉微信开发者工具一系列的上传流程,从而导致影响开发进度、发布流程不可控等一系列问题...

default

如果小程序可以通过Jenkins集成,将大大解放开发者的生产力,将Jenkins权限收回到测试同学手里,每次发布都需要测试同学执行构建,一定程度上解决发布过程不可控的问题。

小程序持续集成的限制

微信开发者工具目前只支持Mac和Windows环境,可以在单独的Mac mini机器上搭一个Jenkins环境,专门用于打包ios app和小程序。

前期知识储备

除了图形化工具,开发者工具还提供了命令行与 HTTP 服务两种接口供外部调用,开发者可以通过命令行或 HTTP 请求指示工具进行登录、预览、上传等操作。官方文档

我们先尝试使用命令行工具启动并登录微信开发者工具:

命令行工具所在位置:

macOS: <安装路径>/Contents/MacOS/cli

Windows: <安装路径>/cli.bat

以下示例皆运行在MacOS 10.13.5环境:

如果微信开发者安装在应用程序里面,其中<安装路径>则为 /Applications/wechatwebdevtools.app

启动微信开发者工具

终端执行/Applications/wechatwebdevtools.app/Contents/MacOS/cli -o,结果如下:

image

并且开发者工具已经启动:

lalpdgq9qca_-cvnadrnau4_334_474 png_620x10000q90g

ps: mac环境下如果是新安装的开发者工具,一定要先打开并通过安全验证。

登录微信开发者工具

终端执行/Applications/wechatwebdevtools.app/Contents/MacOS/cli -l,结果如下:

lalpdgq9qcbcguvnatxnar0_701_725 png_620x10000q90g

扫描二维码后,终端打印login success,并且此时开发者工具已经登录:

default

我们尝试使用HTTP 服务预览项目:

在启动和登录开发者工具后,我们需要获取工具运行所在的端口号(端口是不固定的),然后调用http服务预览此项目,执行以下命令获取端口号:

port=$(cat "/Users/pengyong/Library/Application Support/微信web开发者工具/Default/.ide")
echo "微信开发者工具运行在${port}端口"

假设我的项目地址在:/User/demo,开发者工具开启在55228端口,在浏览器输入http://127.0.0.1:55228/preview?projectpath=/User/demo,可以查看预览二维码

image

我们知道开发者可以通过命令行或 HTTP 请求指示工具进行启动、登录、预览等操作,接下来就进入正题:安装和部署Jenkins。

配置Jenkins

Jenkins运行依赖java环境,终端输入java -version,看输出是否正确比如:java version "1.8.0_151"

一般Mac安装Jenkins有两种方法:

  1. Jenkins官网(https://jenkins.io/ )下载安装包,一路Next。
  2. Tomcat + War

这里我推荐第二种安装方式,因为第一种方式会生成一个共享的用户Jenkins,接下来所有构建的操作都是基于Jenkins这个用户的,它的权限与你当前登录的系统用户权限不同,导致构建过程中出现很多问题。

安装Tomcat

  1. 官网下载Tomcat安装包,我下载的是apache-tomcat-8.5.37.tar.gz这个版本,重命名为Tomcat8,放在/Users/用户/Library这个目录下。
  2. 将Tomcat的bin路径添加到环境变量中:
sudo vi ~/.bash_profile
export PATH=$JAVA_HOME/bin:$PATH:/Users/pengyong/Library/Tomcat8/bin
source ~/.bash_profile
  1. 为了避免权限问题,给你的Tomcat/bin/*.sh分配权限

sudo chmod 755 /Users/pengyong/Library/Tomcat8/bin/*.sh

  1. 查看8080端口是否被占用,使用kill PID解除占用
lsof -i:8080
kill PID
  1. 终端输入startup.sh,如果出现下图所示内容,则启动成功

image

  1. 浏览器访问http://localhost:8080/

image

  1. 关闭Tomcat使用shutdown.sh

至此,Tomcat的安装基本上完成了,接下来安装Jenkins

安装Jenkins

1. 官网下载Jenkins war包,放入你的Tomcat/webapps目录下

image

  1. 浏览器访问http://localhost:8080/jenkins

  2. 初次访问会让你输入密码,可以根据路径提示获取密码

image

如果文件提示没有权限无法打开,需要先修改权限,如下:

image

  1. 输入密码,点击continue,进入插件安装页面

image

  1. 点击推荐安装,等待安装完成

image

  1. 安装成功后,进入创建Jenkins用户界面,填写完成点击Save and Continue

image

  1. 用户创建完成后进入配置Jenkins URL界面,你可以将其修改成你期望的地址,然后点Save and Finish

image

  1. 到了这个界面,恭喜你设置Jenkins成功,但是我们还差最后一步:重启Jenkins

image

  1. 浏览器访问http://localhost:8080/jenkins/restart(你的jenkins地址+restart),点击Yes重启

image

有可能页面一直展示loading,你可以尝试直接访问Jenkins主页,如果出现这个页面,Jenkins的安装过程到此结束,你可以创建任务了。

image

  1. 插件安装:系统管理 - 插件管理 - 可选插件(Available)

Git parameter: 能够实现选择指定分支进行构建的功能
description setter: 用于生成预览二维码

至此准备工作已完成,让我们开始构建小程序吧。

任务构建-配置

  1. 我们新建一个名为wechat的任务,选择构建一个自由风格的软件项目,点击ok进入到配置界面

image

  1. General配置选择参数化构建过程

build_type用于选择构建的是开发版、测试版还是生产版的小程序

image

branch用于选择构建的分支(如果没有这个选项,检查Git parameter这个插件有没有安装)

image

upload_descupload_version两个文本参数分别用于在构建时填写小程序的备注和版本

image

  1. 源码管理选择Git,填上仓库地址,分支这里默认是master,改成我们构建时选择的分支(注:本地生成的 id_rsa.pub 添加到git仓库的ssh认证,否则jenkins无法连接git)

image

  1. 构建选择执行shell

脚本如下,可以按需修改:

echo -------------------------------------------------------
echo GIT_BRANCH: ${GIT_BRANCH}
echo -------------------------------------------------------
# 执行项目构建
yarn install 
if [ "$build_type" == "dev" ]
  then
  yarn run test
else
  yarn run $build_type
fi
# 打开微信开发者工具
/Applications/wechatwebdevtools.app/Contents/Resources/app.nw/bin/cli -o
port=$(cat "/Users/pengyong/Library/Application Support/微信web开发者工具/Default/.ide")
echo "微信开发者工具运行在${port}端口"
return_code=$(curl -sL -w %{http_code} http://127.0.0.1:${port}/open)
if [ $return_code == 200 ]
  then
  echo "返回状态码200,devtool启动成功!"
else
  echo "返回状态码${return_code},devtool启动失败"
  exit 1
fi
if [ "$build_type" == "dev" ]
  then
  echo "发布开发版!"
  # wget -o下载预览二维码,以build_id命名
  /usr/local/bin/wget -O $BUILD_ID.png http://127.0.0.1:${port}/preview?projectpath=/Users/pengyong/.jenkins/workspace/wechat
  echo "预览成功!请扫描二维码进入开发版!"
elif [ "$build_type" == 'prod' ] || [ "$build_type" == "test" ] || [ "$build_type" == "test:demo" ]
  then
  echo "准备上传!"
  # 上传到微信平台
  /Applications/wechatwebdevtools.app/Contents/Resources/app.nw/bin/cli -u $upload_version@/Users/pengyong/.jenkins/workspace/wechat --upload-desc $upload_desc
  echo "上传成功!请到微信小程序后台设置体验版或提交审核!"
fi
  1. 构建后操作选择Set build description(没有则检查description setter插件是否安装)

image

这里使用img标签用于展示构建后的二维码(注:端口号是你jenkins启动的端口,任务名称是我们任务构建这一步填写的名称,这里我们是wechat):

<img src="http://本机ip:端口/job/任务名称/ws/${BUILD_ID}.png" alt="非开发版请到后台预览" width="200" height="200" /> <a href="http://本机ip:端口/job/任务名称/ws/${BUILD_ID}.png" target="_blank">二维码${BUILD_ID}</a>

至此我们的任务构建配置基本完成了

任务构建-预览小程序

  1. 选择Build with parameters,选择你要构建的类型、分支等参数,这里我们选择的是开发版:

image

  1. 构建完成后发现二维码以文本的形式展示,没有展示图片

image

解决的方法是在系统管理 -> 全局安全配置 -> 标记格式器 -> 选择Safe HTML

image

这时预览二维码就出来了,注意二维码是有过期时间的(35min)

image

至此预览二维码的构建任务已经完成,我们尝试上传代码到微信平台

任务构建-上传代码

  1. 依然是选择构建类型、分支,不同的是上传代码需要填小程序版本号和项目备注,然后执行构建

image

  1. 到运营平台或开发平台查看提交的版本

image

友情提示

  1. 开发者工具必须在登录状态下进行预览、上传的操作,为了避免账户冲突,可以在Jenkins服务器上使用专门的微信账号进行登录,这个账户要开启开发者和体验者权限,尽量不要使用开发者的账号。
  2. 不要直接在官网下载Jenkins安装运行,坑真的很多,博主是含着泪才走通流程的T.T

总结

目前小程序的发布大多还是依赖开发者手动上传,并且小程序持续集成还是有很多问题:开发者工具不支持Linux环境、无法通过命令行生成体验版二维码等等。但是持续集成这个方向还是值得大家研究的,开发和发布要两开花嘛...

参考:

typescript手册-对象的类型-接口

typescript中,我们使用接口(interface)描述对象的类型

什么是接口

在面向对象语言中,接口(interface)是很重要的概念,它是对行为的抽象,而具体如何行动由类(class)去实现(implement)

typescript中,接口不仅可以对类的行为进行抽象,也可以对对象的形状进行描述

用接口描述对象

我们通过一个简单的例子,来看接口如何对对象的形状进行描述的:

interface Person {
  name:string;
  age:number
}

let student:Person = {
  name: 'zhangsan',
  age: 18
};

上面的例子中,我们定义了接口Person,然后定义了变量student,变量的类型是Person,这样我们就约束了student的形状必须跟Person一致。即必须有nameage两个属性。

定义的对象,多一些属性是不被允许的:

interface Person {
  name:string;
  age:number
}

let student:Person = {
  name: 'zhangsan',
  age: 18,
  address: 'hangzhou'
};

我们为student对象新增了一个属性address,编译报错:

Type '{ name: string; age: number; address: string; }' is not assignable to type 'Person'.
  Object literal may only specify known properties, and 'address' does not exist in type 'Person'

意思是变量不能指定Person类型,因为变量只能指定在Person中的已知属性,而address不存在于类型Person

这里我们注意到{ name: string; age: number; address: string; }typescriptstudent做的类型推导,关于类型推导,我们后面再聊。

定义的对象,少一些属性也是不被允许的:

interface Person {
  name:string;
  age:number
}

let student:Person = {
  name: 'zhangsan'
};
Type '{ name: string; }' is not assignable to type 'Person'.
  Property 'age' is missing in type '{ name: string; }'

可见,接口Person要求对象一定具备nameage两个属性,即形状必须保持一致。

centos8.2安装docker

系统 Centos8.2

[lukou@deploy ~]$ lsb_release -a
LSB Version:  :core-4.1-amd64:core-4.1-noarch
Distributor ID: CentOS
Description:  CentOS Linux release 8.2.2004 (Core)
Release:  8.2.2004
Codename: Core

Docker安装

• step 1:安装一些必要的工具
sudo yum install -y yum-utils device-mapper-persistent-data lvm2
• step 2:添加软件源信息
sudo yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
• Step 3: 更新 Docker-CE

sudo yum makecache fast
• 错误: yum makecache: error: argument timer: invalid choice: ‘fast’ (choose from ‘timer’)
• 问题: ‘索引的时候出错,即centos8没有该参数,解决办法为:去掉fast参数’
• 命令改成: sudo yum makecache
• 再次执行: sudo yum makecache

• Step 5: 开启Docker服务

• 启动1: sudo service docker start
• 启动2: sudo systemctl start docker

• Step 4: 安装 Docker-CE

sudo yum -y install docker-ce
• 报错: Problem: package docker-ce-3:19.03.12-3.el7.x86_64 requires containerd.io >= 1.2.2-3, but none of the providers can be installed;
• 根据提示,需要containerd.io的版本 >= 1.2.2-3,操作如下:
• 安装wget指令 yum install wget;
• 获取rpm包 wget https://download.docker.com/linux/centos/7/x86_64/edge/Packages/containerd.io-1.2.6-3.3.el7.x86_64.rpm;
• 升级containerd.io(安装rpm包) yum -y install containerd.io-1.2.6-3.3.el7.x86_64.rpm;
• 再次执行 sudo yum -y install docker-ce;

• Step 6: 安装效验

• docker version
• sudo docker ps

• 设置开机自启动
sudo systemctl enable docker
• 重启生效
systemctl restart docker.service

使用Mongoose操作Mongodb数据库

名词解释

  • Schema: 用于定义数据库的结构,类似创建表时的数据定义,不具备数据库的操作能力
  • Model: 由Schema编译而成的假想(fancy)构造器,具有抽象属性和行为,可以对数据库进行增删盖茶,Model的每一个实例(instance)就是一个document。document可以保存到数据库和从数据库检索。
  • Instance: 由Model创建的实例。

概念解析

image

简介

Mongoose是Nodejs环境下对Mongodb进行便捷操作的工具,使用之前需要先安装MongodbNodejs,推荐一款mongodb的可视化工具Robomongo

Mongoose的安装

npm i mongoose
安装成功之后可以通过require('mongoose')使用,并且可以在package.json的dependencies看到当前版本号

创建一个简单的数据库连接

创建mongoose.js,执行以下代码node mongoose.js

const mongoose = require('mongoose')

const db = 'mongodb://localhost:27017/test'
mongoose.set('debug', true)

// 连接数据库
mongoose.connect(db)

// 连接中断,重新连接
mongoose.connection.on('disconnect', () => {
  mongoose.connect(db)
})

// 连接出错
mongoose.connection.on('error', (err) => {
  console.error(err)
})

// 在连接成功并且本次连接的所有models里面的onOpen执行完成之后执行
mongoose.connection.on('open', () => {
  console.log('Connect on mongodb open', db)
})

// 成功连接到数据库时触发,重新连接的时候可能会被多次触发
mongoose.connection.on('connected', () => {
  console.log('Connect on mongodb connected', db)
})

连接成功后先执行connected的回调再执行open的回调,所有的events参考http://www.nodeclass.com/api/mongoose.html#connection_Connection

Schema

Schema是Mongoose中用到的一种数据模式,可以理解成表结构的定义

通过mongoose.Schema来调用Schema,然后使用new方法创建schema对象

例如定义一个user的Schema

mongoose.js

module.exports = mongoose

user.js

const mongoose = require('./mongoose');
const schema = mongoose.Schema;

const UserSchema = new schema({
  account: String,                              // 登录账户
  password: String,                             // 登录密码
  age: Number,                                  // 年龄
  lastlogin: { type: Date, default: Date.now }  // 上次登录时间
})

定义一个Schema需要指定字段名和类型,允许的SchemaTypes如下

  • String
  • Number
  • Date
  • Buffer
  • Boolean
  • Mixed
  • ObjectId
  • Array
    关于它们的更多介绍在这里

Model

定义好Schema之后,接下来就是生成Model,Model是由Schema生成的模型,用于对数据库的操作,model()的第一个参数是模型名称

根据UserSchema生成一个model并导出,修改后的代码如下:

const mongoose = require('./mongoose');
const schema = mongoose.Schema;

const UserSchema = new schema({
  account: String,                              // 登录账户
  password: String,                             // 登录密码
  age: Number,                                  // 年龄
  lastlogin: { type: Date, default: Date.now }  // 上次登录时间
})

model.exports = mongoose.model('User', UserSchema)

常用数据库操作

创建一个test.js,进行一些简单的数据库操作

  1. 插入数据document#save([fn])
const User = require('./user.js');

function insertUser() {
  // 构建documents
  let user = new User({
    account: 'a123456',
    password: 'aaaaaa',
    age: 18,
    lastlogin: new Date()
  });

  user.save((err, res) => {
    if (err) {
      console.log(err)
    } else {
      console.log('res:'+res)
    }
  });
}

insertUser();

结果在Robomongo工具中查看
image

  1. 更新Model.update(conditions, update, [optinos], [callback])
const User = require('./user.js');
const Util = require('util');

function update() {
 let whereStr = {"account": "a123456"};
 let updateStr = {"password": "111111"};

 User.update(whereStr, updateStr, (err, res) => {
   if (err) {
    console.log('err'+err)
   } else {
     console.log('res'+Util.inspect(res))
   }
 });
}

update();

根据账号更新密码,结果如图:
image
更新数据常用方法还有findByIdAndUpdate(id, [update], [options], [callback])

const User = require('./user.js');
const Util = require('util');

function findByIdAndUpdate() {
 let id = '5a91306ef88eea0b14532731';
 let updateStr = {"password": "abcdef"};

 User.findByIdAndUpdate(id, updateStr, (err, res) => {
   if (err) {
    console.log('err'+err)
   } else {
     console.log('res'+Util.inspect(res))
   }
 });
}

findByIdAndUpdate();

3、删除Model.remove(conditions,[callback])

const User = require('./user.js');
const Util = require('util');

function del() {
 let whereStr = {"account": "a123456"};
 User.remove(whereStr, (err, res) => {
   if (err) {
    console.log('err'+err)
   } else {
     console.log('res'+Util.inspect(res))
   }
 })
}

del();

结果会在对应数据表中删除匹配到的数据,res结果res{ n: 1, ok: 1 },其中n为影响到的行数,ok为是否成功
其他常用方法: Model.findByIdAndRemove(id, [options], [callback])
4、条件查询Model.find(conditions, [fields], [options], [callback])

const User = require('./user.js');
const Util = require('util');

function getByConditions() {
  let whereStr = {"account": "a123456"};

  User.find(whereStr, (err, res) => {
    if (err) {
      console.log('err'+err)
    } else {
      console.log('res'+Util.inspect(res))
    }
  });
}

getByConditions();

找到的结果以数组形式返回
image
第二个参数可以设置查询输出的字段,比如:

const User = require('./user.js');
const Util = require('util');

function getByConditions() {
  let whereStr = {"account": "a123456"};
  let field = {"account": 1, "password": 1, "_id": 0}
  User.find(whereStr, field, (err, res) => {
    if (err) {
      console.log('err'+err)
    } else {
      console.log('res'+Util.inspect(res))
    }
  });
}

getByConditions();

输出字段只有account和password,1表示输出,0表示不输出
如果要查询年龄段,条件范围应该怎么写?
User.find({"age": {$gte: 18, $lte: 40}, [field], [options], [callback])
image
其实类似的还有: 

  $or    或关系

  $nor    或关系取反

  $gt    大于

  $gte    大于等于

  $lt     小于

  $lte    小于等于

  $ne 不等于

  $in 在多个值范围内

  $nin 不在多个值范围内

  $all 匹配数组中多个值

  $regex  正则,用于模糊查询

  $size   匹配数组大小

  $maxDistance  范围查询,距离(基于LBS)

  $mod   取模运算

  $near   邻域查询,查询附近的位置(基于LBS)

  $exists   字段是否存在

  $elemMatch  匹配内数组内的元素

  $within  范围查询(基于LBS)

  $box    范围查询,矩形范围(基于LBS)

  $center 范围醒询,圆形范围(基于LBS)

  $centerSphere  范围查询,球形范围(基于LBS)

  $slice    查询字段集合中的元素(比如从第几个之后,第N到第M个元素)

查询条件也可以使用正则表达式,即模糊查询
User.find({"account": /7$/}, [field], [options], [callback])
image
查看查询条件更多规则

5、查询数量Model.count(conditions, [callback])

const User = require('./user.js');
const Util = require('util');

function getCountByConditions() {
  let whereStr = {};
  User.count(whereStr, (err, res) => {
    if (err) {
      console.log('err'+err)
    } else {
      console.log('res'+Util.inspect(res))
    }
  });
}

getCountByConditions();

res会输出数量
6、根据_id查询Model.findById(id, [fields], [options], [callback])

const User = require('./user.js');
const Util = require('util');

function getById() {
  let id = '5a916eee7bbb870ba20cf581';
  User.findById(id, (err, res) => {
    if (err) {
      console.log('err'+err)
    } else {
      console.log('res'+Util.inspect(res))
    }
  });
}

getById();

7、分页查询

const User = require('./user.js');
const Util = require('util');

function getByPaper(page) {
  let size = 2; // 每页显示数量
  let currentPage = page; // 当前页数
  let sort = {"lastlogin": -1}; // 排序方法,按照登录时间倒叙排列
  let condition = {};
  var skipNum = (currentPage - 1) * size; // 跳过数

  User.find(condition).skip(skipNum).limit(size).sort(sort).exec((err, res) => {
    if (err) {
      console.log('err'+err)
    } else {
      console.log('res'+Util.inspect(res))
    }
  });
}

getByPaper(1);
getByPaper(2);
getByPaper(3);

jenkins构建h5项目

创建项目

image

参数化构建

如果构建的过程中需要选参数:
image
勾选This project is parameterized
通常我们只会用到Choice Parameter和Git参数(需要安装Git Parameter Plug-In)两种选项:
• Choice Parameter:提供select选择功能
• Git参数:可以自行选择要构建的分支
image

Choice Parameter

• 名称:当前选中的值会赋值给变量SERVER_IP,在shell中通过$SERVER_IP获取
• 选项:select选择框提供的选项,默认选中第一个值
image
实际效果如下:
image

Git参数

• 名称:当前选中的分支会赋值给变量SERVER_IP,在shell中通过$SERVER_IP获取
• 参数类型:分支
• 默认值 + 已选值:指定默认选中哪个分支
image
实际效果如下:
image

源码管理Git

配置远程仓库地址

通常我们配置git仓库地址会报错:
image
需要我们在服务器上生成的id_rsa.pub添加到gitlib的ssh keys下面,具体做法如下:
新的解决方案:
将宿主机的ssh文件拷贝到docker容器中,共享一份ssh key

// 将宿主机的/home/lukou/.ssh拷贝到jenkins容器的/root目录下
sudo docker cp /home/lukou/.ssh jenkins:/root

旧的解决方案:
以在docker镜像为例,如果直接在服务器装的jenkins可以免去这一步:

  1. 进入jenkins(这里是你的容器名)容器,查看有没有id_rsa.pub文件
docker exec -it jenkins /bin/bash
cd ~/.ssh
cat id_rsa.pub
#如果没有该文件需要我们手动创建,一路回车即可
ssh-keygen -t rsa -C "[email protected]"
  1. cat id_rsa.pub 复制到gitlab上
  2. 测试是否配置成功(这一步会在~/.ssh_keys生成know_hosts,不能跳过)
    git ls-remote -h -- [email protected]:frontend/bumblebee.git
  3. 刷新页面看到已经验证成功
    image

指定分支

指定当前构建的分支为$BRANCH,即参数化构建 - 用户手动选中的分支

限制git分支

如果我们生产环境只能构建master分支

  1. 在Git参数 -> 高级这里,设置默认值,这样构建时只能选择master分支
    image
  2. 在shell脚本里面判断当前分支是不是origin/master
# 判断分支
if [ "$BRANCH" != "origin/master" ];then
echo "当前分支是$BRANCH,生产环境必须是master分支,退出构建"
exit 1
fi

# 构建
nvm use v12.9.1
npm -v
node -v

构建shell

#!/bin/bash -ilex
# set enviroment
PROD_NAME="dist"
PROD_ZIP="$PROD_NAME.zip"

USER="lukou"
APP_IP=$SERVER_IP
APP_HOME="/home/lukou/tmp/$DIRECTORY"

# 构建
nvm use v12.9.1
npm -v
node -v
npm config set registry https://registry.npm.taobao.org
npm install
npm run build

zip -rp $PROD_ZIP ./$PROD_NAME
echo $PWD
scp $PROD_ZIP $USER@$APP_IP:$APP_HOME/
scp template/* $USER@$APP_IP:$APP_HOME/

# 开始部署
ssh -T $USER@$APP_IP << EOF

 #sudo chmod 777 -R $APP_HOME
 cd $APP_HOME
 unzip -oq $PROD_ZIP
 rm -rf ./static
 rm -rf ./index.html
 mv ./dist/* ./
 rm -rf ./$PROD_NAME
 rm -rf ./$PROD_ZIP
EOF

常见问题

  1. 发送构建文件到目标服务器报错
+ scp dist.zip [email protected]:/home/lukou/tmp/coupon_qiuyu/
Host key verification failed.
lost connection
Build step 'Execute shell' marked build as failure
Finished: FAILURE

原因:
猜测是因为当前docker容器没有连接过目标服务器所致,查看目标服务器~/.ssh/know_hosts,确实没有当前docker容器的id_rsa.pub,一般连接过的都会在这里产生记录
解决办法:
如果jenkins在docker容器中,此操作在容器中进行
• 复制~/.ssh/id_rsa.pub
• 登录目标服务器

ssh [email protected]
cd ~/.ssh
vi authorized_keys
添加复制的id_rsa.pub

回到docker容器或者原服务器中,使用ssh连接目标服务器即可

root@cb0c931a18d2:~/.ssh# ssh [email protected]
The authenticity of host '121.41.121.230 (121.41.121.230)' can't be established.
RSA key fingerprint is SHA256:KI315BM3JQvdx7RpazSER7WUD4SWPe6KAs5G/lPOynA.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '121.41.121.230' (RSA) to the list of known hosts.
Last login: Sat Jul 11 09:12:17 2020 from 120.26.131.154

Welcome to aliyun Elastic Compute Service!

typescript手册-类型断言

类型断言指手动指定一个值的类型,一般用来告诉typescript编译器:我已经明确知道这个值的类型是什么

语法

<类型>值值 as 类型

简单的例子

高级类型一节中,我们提到当一个联合类型的变量不能确定是哪个类型的时候,我们只能访问所有类型公有的属性和方法

function getLength(something: string | number): number {
  return something.length;
}
// Property 'length' does not exist on type 'string | number'.
//  Property 'length' does not exist on type 'number'

但有时候,我们确实需要在不明确变量类型的时候,就访问它的属性或方法,比如:

function getLength(something: string | number): number {
  if (something.length) {
    return something.length;
  } else {
    return something.toString().length;
  }
}
// Property 'length' does not exist on type 'string | number'.
//  Property 'length' does not exist on type 'number'

上例中,我们访问something.length编译会报错,此时可以使用类型断言,将something的类型断言成string

function getLength(something: string | number): number {
  if ((<string>something).length) {
    return (something as string).length;
  } else {
    return something.toString().length;
  }
}

上面对两种写法都做了演示,实际生产中最好统一语法。

类型断言,不能断言成联合类型中不存在的类型,比如这里将something断言为boolean类型,不在string|number中,编译将会报错

function toBoolean(something: string | number): boolean {
  return <boolean>something;
}
Conversion of type 'string | number' to type 'boolean' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
  Type 'number' is not comparable to type 'boolean'

其中neither type sufficiently overlaps with the other也提示我们:两种类型都没有与另一种类型充分重叠。

参考:

记一次vue渲染白屏的hack方法

复现问题步骤

  1. 首页滑动一段距离之后点击list,进入到详情页

2.由于单页面共享一个浏览器滚动条,并且页面通过keep-alive缓存,导致详情页没有正确回到顶部
wechatimg470

3.为了避免上述情况,在detail页面强制滚动到顶部
window.scrollTo(0, 0)

4.从详情页返回到首页,由于每次返回到首页,首页都会重新请求数据更新页面,导致页面出现白屏,如果轻拂一下页面,就可以正常展示了
wechatimg472

解决方法:

参考vue的issue,在首页请求数据之后,动态的设置scrollTop

this.$nextTick(() => {
    window.scrollTo(0, 1)
    window.scrollTo(0, 0)
})

记一次vue.js的**事件总线event bus重复执行的bug

需求分析

list页面点击列表的某一项(比如活动列表),进入到detail页面进行编辑,list页面和detail页面关系是这样:

{
  path: '/list',
  component: List,
  children: [
      path: 'detail',
      component: Detail
  ]
}

detail页面返回到list页面时,希望刷新整个列表,但不刷新list页面(list页面还有其他的数据)

最初的方法

  1. 定义event bus,分别在list页面和detail页面引入

  2. 在list的created钩子函数里面监听刷新事件

Bus.$on('refreshList', () => {
      // ... 这里请求接口刷新列表
 })
  1. 在detail页面的beforeDestroy钩子函数里面触发数据
    Bus.$emit('refreshList')

遇到的问题

一切看起来没问题,但是产品要求,点击菜单栏要求刷新当前页面(非刷新浏览器),目前的做法是通过vuex动态修改顶级router-view的显示隐藏
image
这就导致每点击一次侧边栏,页面重新渲染一次,created函数重新执行一次,refreshList事件就重新绑定一次(event bus是不会随着页面销毁而销毁的,需要手动销毁,查看vue issue),导致从detail页面返回list页面的时候,refreshList的回调函数请求了n+1次(比如点击了n次侧边栏)

手动销毁event bus

通过watch路由变化,希望销毁对refreshList的监听,this.callback是对刷新函数的一次封装

watch: {
      '$route': function () {
        Bus.$off('refreshList',this.callback)
      }
}

官网也有对off事件的介绍
image
但是无论重复绑定多少次refreshList事件,来回切换list和detail页面,永远只销毁一次

追根溯源

image
查看vue源码的$off方法,它会判断传入的event事件是数组函数字符串,然后通过Object.create(null)新建一个对象,key是index,value是事件处理函数this.callback,形如:

{
 0: this.callback,
 1: this.callback
}

最后遍历该对象,移除对应的callback
最终发现它确实只销毁一次,这样对应了官网api第三条:如果同时提供了事件与回调,则只移除这个回调的监听器。

解决方法

通过在list页面定义refresh方法,在detail页面的beforeDestroy钩子函数调用this.$parent.refresh()刷新页面,也看出event bus不适用于父子组件,官网也提到适用于兄弟组件的数据通讯

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.