手写v-model之简造

** 很多都是自己的理解,”谨”供参考,如果错误,烦请指正

什么是双向绑定

 双向绑定的概念最早是从Angularjs1.0版出现,这也是当年Angularjs能称为顶级框架的原因之一
 所谓的双向绑定,实际是为了实现数据与视图的”联动”
 它的一端连着数据仓库,另一端连着视图,而它站在中间帮你完成所有的同步(绑定)工作
 你要做的,就是向上帝管理河流一样,保证你数据格式正确、清晰它的来源和去向、以及保证每种数据变化的合理
 当然,这不代表你应该过分控制你的河流

 想要将数据仓库与试图同步起来,你要做2件事,这就是双向绑定的核心任务
    ① 数据变动,通知视图变化
    ② 视图变化,修改数据仓库

 第2件事是我们最擅长的,监听视图即事件监听,修改相应的仓库数据
 而第1件事,想要监听数据变动,我们就需要借助相应的API
 下一步,我们了解一下可以监听数据的两个API:ES5的Object.defineProperty 与ES6的Proxy

Object.defineProperty

 Object.defineProperty(obj, prop, descriptor)
   obj:要在其上定义属性的对象
   prop:要定义或修改的属性的名称
   descriptor:将被定义或修改的属性描述符
      ① configurable: 该属性描述符是否可通过delete删除,默认false
      ② enumerable: 是否可枚举,默认false(不可枚举属性无法通过for…in遍历)
      ③ value: 属性值,默认undefined
      ④ writable: 是否可被赋值运算符改变,默认false
      ⑤ get: 方法名,属性被获取时调用,默认undefined
      ⑥ set: 方法名,属性被改变时调用,默认undefined

 使用方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

// 监听对象 obj 监听属性 name
const obj = {};
Object.defineProperty(obj,'name',{
get: function () {
// 此处获取对象属性值
return this._name;
},
set: function (newValue) {
this._name = '皮一下 ' + newValue;
}
});

obj.name = '张三'';
console.log(obj.name);

 defineProperty可以劫持数据,但只能监听对象中的某个属性
 IE8以下不支持该属性
 Vue底层通过defineProperty实现双向绑定

Proxy

 new Proxy(target, handler);
   ① target: 监听对象
   ② handler: 操作对象
   ③ 返回该对象
  ES6吸收了强类型语言的精华,为JavaScript新增了代理Proxy与反射Reflect
  Proxy可以监听整个对象的变化

  用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var obj = new Proxy({},{
get (target,propKey,receiver){
console.log('获取属性'+propKey + '的值');
return Reflect.get(target,propKey,receiver);
},
set(target,propKey,value,receiver){
// target: 监听对象
// target下的改变的属性名
// 新赋值
// Proxy对象
return Reflect.set(target,propKey,'皮一下 '+value,receiver);
}
});

// 注: obj是Proxy对象

obj.name = '张三';
console.log(obj.name);

简单数据绑定

 初级数据绑定,我们只需要满足数据与视图的捆绑即可

1
2
3
4
<div id="app">
<div id="divText">{{name}}</div>
<input id="inputText" />
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

// 监听对象
const obj = { name: 'HelloWorld' };

// 获取DOM元素
const $divText = document.querySelector('#divText');
const $inputText = document.querySelector('#inputText');

// 页面渲染逻辑
function pageInit(target){
$divText.innerHTML = obj[target] || '';
$inputText.value = obj[target] || '';
}

// 初始化视图
pageInit('name');


Object.defineProperty(obj,'name',{
get(){
return obj._name;
},
set(newValue){
obj._name = newValue;

// 粗暴通知视图变化
pageInit('name');
}
});

// 监听事件
$inputText.addEventListener('input',function(){
obj.name = this.value;
},false)

仿源码建造数据绑定

 仿Vue搭建可扩展的数据绑定,调用如下

1
2
3
4
5
6
// 无需再为DOM绑定ID
<div id="app">
<div>姓名:{{name}}</div>
<div>年龄:{{age}}</div>
<input type="text" v-model=' name ' />
</div>

1
2
3
4
5
6
7
Vue({
el: '#app', // 根DOM
data: { // 绑定数据
name: '小明',
age: 18
}
})

 Vue与字符串模版相似,将根DOM下的元素经过处理后将其替换
 但不同于简单粗暴的字符串模版,Vue会通过DOM Diff算法来进行优化
 首先,我们初始化页面,将初始数据置入,并替换DOM

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

// DOM结点处理
function cloneNode(node,vm) {
const flag = document.createDocumentFragment();
let child;

while(child = node.firstChild) {
compile(child,vm);
flag.appendChild(child);
}

return flag
}


// Vue 构造函数
function Vue(opts){
this.$data = opts.data || {};
const _root = document.querySelector(opts.el || '#app');
const _dom = cloneNode(_root,this);
_root.appendChild(_dom);
}


// 处理模版DOM中的绑定数据
function compile(node,vm) {
const attr = node.attributes;

// 元素为input且双向绑定
if(~~node.nodeType === 1 && node.nodeName == 'INPUT' && attr['v-model']) {
const _name = attr['v-model'].value.trim();
node.value = vm.$data[_name];
node.addEventListener('input', function () {
// 更新后的数据绑定到了vm中,而不是vm.$data;
vm[_name] = this.value;
},false);
node.removeAttribute('v-model');
}else {
// 全局替换对象
node.nodeValue = node.nodeValue.replace(/\{\{(.*)\}\}/g, function (reg,$1) {
return vm.$data[$1.trim()];
})
}
}

 我们已经绑定了input事件,及时更改了Vue实例对象中的数据值,但我们还没有做数据监听

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function Vue(opts){
this.$data = opts.data || {};
const _id = document.querySelector(opts.el || "#app");

observe(this.$data,this); // 新加

const _dom = cloneNode(_id,this); // 获取劫持DOM
_id.appendChild(_dom); // 将替换为劫持后的DOM
}


function observe() {
Object.keys(obj).forEach(key=>{
defineReactive(vm,key,obj[key]);
})
}


// 数据监听处理 obj--> 历史值
function defineReactive(vm,key,val) {
Object.defineProperty(vm,key,{
get(){
//
return val;
},
set(newValue){
if(newValue == value) return;
val = newValue;

// 视图更新
// 留白
}
})
}

 视图更新我们用发布-订阅者模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

// 发布者
function Dep(){
this.subs = [];
}

Dep.prototype = {
// 添加监听对象
addSub(sub){
this.subs.push(sub);
},
// 通知所有被监听者
notify(){
this.subs.forEach(sub=>sub.updata());;
},
};

// 订阅者
function Watcher(vm,node,name,nodeType){
this.vm = vm;
this.node = node;
this.name = name;
this.nodeType = nodeType;
};

Watcher.prototype = {
// 更新视图
update(){
// 获取最新数值
this.value = this.vm[this.name];
// 更新相应的DOM
}
};

 最后就是何时订阅,何时发布了
 DOM处理数据时->订阅,监听到数据变化是->发布
 此外,将监听数据全部扩展到vm中
 完整代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
function Dep(){
this.subs = [];
}

Dep.prototype = {
addSub(sub){
this.subs.push(sub);
},
notify(){
this.subs.forEach(sub=>sub.update());
},
};


function Watcher(vm,node,name,nodeType){
this.vm = vm;
this.node = node;
this.name = name;
this.nodeType = nodeType;
};

Watcher.prototype = {
update(){
this.value = this.vm[this.name];
if(this.nodeType == 'input'){
this.node.value = this.value;
}else{
this.node.nodeValue = this.value;
}
}
};

let dep = new Dep();

function cloneNode(node,vm) {
const flag = document.createDocumentFragment();
let child;

while(child = node.firstChild) {
compile(child,vm);
flag.appendChild(child);
}

return flag;
}

function compile(node,vm) {
const attr = node.attributes;
let nodeType = 'text';
let _name;

// 元素为input且双向绑定
if(~~node.nodeType === 1 && node.nodeName == 'INPUT' && attr['v-model']) {
_name = attr['v-model'].value.trim();
node.value = vm.$data[_name];
node.addEventListener('input', function () {
// 更新后的数据绑定到了vm中,而不是vm.$data;
vm[_name] = this.value;
},false);
node.removeAttribute('v-model');
nodeType = 'input'
}else {
// 全局替换对象
node.nodeValue = node.nodeValue.replace(/\{\{(.*)\}\}/g, function (reg,$1) {
_name = $1.trim();
return vm[$1.trim()];
})
};

dep.addSub(new Watcher(vm,node,_name,nodeType));

}


function observe(obj,vm){
Object.keys(obj).forEach(key=>{
defineReactive(vm,key,obj[key]);
})
}

function defineReactive(vm,key,val) {
Object.defineProperty(vm,key,{
get(){
return val;
},
set(newValue){
if(newValue == val) return;
val = newValue;
dep.notify();
}
})
}


function Vue(opts){
this.$data = opts.data || {};
const _root = document.querySelector(opts.el || '#app');

observe(this.$data,this);


const _dom = cloneNode(_root,this);
_root.appendChild(_dom);
}

手写Redux之简造

** 很多都是自己的理解,”谨”供参考,如果错误,烦请指正

单向数据流

 React是单向数据流,首先我们讲讲单向数据流
 何为单向数据流,顾名思义,就是数据像河流一样只能从一端流向另一端,而且是地势较高的一端(顶级/父级)-向地势低的一端(子级)流动
 这种数据传输特点保证了数据的纯净性,当我们想”追根溯源”,可以轻易检测到问题症结
 当然,React或Vue都可以通过$emit,使子组件通知父组件变更数据,但它依然不会破坏数据的单一流动

Content

 基于单向数据流的处理方式,使我们想要从顶层将数据传给底层子组件,需要中间跨越层层组件
 假使我们嵌套了N层,意味着我们的数据需要经过N层组件,中间N-2个组件都是毫无意义的数据传声筒
 而这样的情况有很多,像我们的用户数据,公共配置,主题…等等
 Content就是帮我们做全局数据处理的,我们常用的React-redux/React-router都是基于Content
 还记得redux是怎么”劝”我们的嘛?

    - “如果你不知道是否需要 Redux,那就是不需要它。”
    - “只有遇到 React 实在解决不了的问题,你才需要 Redux 。”

 redux解决了我们的公共数据问题,但redux只是工具,没有redux我们一样可以实现功能
 是的没错,就是自己使用Context实现

 我们先来明确React不同数据对象的职责:
    - state: 维护组件内部”产生数据”的信息(即本组件内所用的数据)
    - props: 维护组件外部”传入数据的信息”(即来自父组件的数据)
    - content: 维护跨域组件信息的传递(即全局数据)

 关于Redux语法

  • Content创造者:
    • 在产生参数的最顶级组件中,使用childContextTypes静态属性来定义需要放入全局参数的类型。
    • 在父组件中,提供状态,来管理数据
    • 声明子组件获取全局参数的方式
  • Content消费者: 任何需要使用Content的组件
    • 在子组件中,使用contentTypes静态属性,声明需要获取父组件放入全局content中的参数类型
    • 在子组件需要的地方,获取全局参数:this.content.全局属性名
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37

      // 创造者
      import React,{Component} from 'react';
      import PropTypes from 'prop-types';

      class App extends Component{
      state = {
      title : 'Content使用Demo',
      };

      static childContextTypes = {
      title: PropTypes.string,
      };

      getChildContext(){
      return {
      title: this.state.title,
      }
      }
      }


      // 消费者
      import React,{Component} from 'react';
      import PropTypes from 'prop-types';

      class ChildComponent extends Component {
      static contextTypes = {
      title: PropTypes.string,
      };

      render() {
      return (
      <div>{ this.context.title }</div>
      );
      }
      }

Redux用法

 Store: 数据存储仓库
    - createStore: 方法,产生Store的实例对象
    - store.getState(): 返回Store对象中的所有数据

1
2
3
4
import {createStore} from 'redux';
const store = createStore(reducer);

const state = store.getState();

    - store.subscribe(),对store数据监听,一旦发生变化就自动执行
    - store.subscribe()返回一个函数,调用此方法可以解除监听

  Action:触发Store中数据变化的接口及处理方式
    - Action是一个对象,其中type属性是必须的

1
2
3
4
const action = {
type: 'ADD_TODO',
payload: 'Learn Redux'
};

    - 改变数据的唯一方法,就是触发Action
    - store.dispatch就是外界触发Action的唯一方法

1
2
3
4
5
6
7
import { createStore } from 'redux';
const store = createStore(fn);

store.dispatch({
type: 'ADD_TODO',
payload: 'Learn Redux'
});

 Reducer: 新数据更新规则
    - Reducer函数一定要是个纯函数

1
2
3
4
5
6
7
8
  const reducer = function (state, action) {

switch (action.ntype) {
case 'action':
return Object.assign({},state,action.payload);
default: return state;
}
};

简造

 第一步 创造一个基本Store

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
    
// 定义一个createStore函数 来管理全局状态数据
const createState = ()=> {
let state = {
title: '简造一个redux'
};

// 抛出获取state数据的方法
const getStore = ()=>state;

// 提供一个dispatch方法,
const dispatch = (action)=>{
switch (action.type) {
case 'DO': state.title=action.payload;break;
default: break;
}
}

return {getStore,dispatch};
};



// 使用Store
const store = createState();
store.getState(); // 获取state数据
store.dispatch({
type :'DO',
payload: '一次修改'
})

 第二步 创造一个reducer并传入Store

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 创建reducer
const changeStateFn = (state,action)=>{
switch (action.type) {
case 'DO': state.title=action.payload;break;
default: break;
}
};



// 修改Store 数据更改不再写死
const createState = (changeState)=> {
let state = {
title: '简造一个redux'
};

const getStore = ()=>state;

// dispatch返回changeState方法,并将数据state和dispatch的参数传入
const dispatch = (action)=>changeState(state,action);
return {getStore,dispatch};
};


// 调用Store 将Reducer传入State
const store = createState(changeStateFn);

 第三步 Store新增数据监听

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const createState = (changeState)=>{
let state = {
title: '简造一个redux'
};

// 收集监听方法
let listeners = [];

// 将所有监听方法存入监听数组
const subscribe = (listener)=> {listeners.push(listener)};

const dispatch = (action)=>{
// 触发数据更新
changeState(state,action);
// 调用所有监听方法
listeners.forEach(listener=>listener());
};

// 抛出方法
return {dispatch,subscribe};

}

 第四步 优化数据更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 返回新state
const changeStateFn = (state,action)=>{
switch (action.type) {
case 'DO': return {
...state,
...action.payload
}
default: break;
}
};


const createState = (changeState)=> {
let state = {
title: '简造一个redux'
};

let listeners = [];

const dispatch = (action)=>{
// 更改state值
sate = changeState(state,action);
listeners.forEach(listener=>listener());
};
};

 简造版redux打造完毕
 稍作整理,将unsubscribe也加入如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
const createStore =  (reducer)=> {
let state = {};
let listeners = [];

const getStore = ()=>state;
const unsubscribe = (index)=>{
if(listeners.length <= index) return;
return ()=> listeners.splice(index,1);
};
const subscribe = (listener)=> {
const length = listener.length;
listeners.push(listener);
return unsubscribe(length);
};
const dispatch = (action)=>{
state = reducer(state,action);
listeners.forEach(listener=>listener());
};

return {getStore,subscribe,dispatch};
};

const changeStateFn = (state,action)=>{
switch (action) {
case 'DO': return Object.assign({},state,action.payload);
default: return state;
}
};

// 使用
const store = createStore(changeStateFn);


const oldState = store.getStore();
store.subscribe(()=>{
const newState = store.getStore();
if(oldState.title !== newState.title) {
console.log('change the title');
}
});

store.dispatch({
type: 'DO',
payload: {title: '修改title'}
});

 下一步,我们去浅析源码

前端常用学习网站

@(welcome)[前端菜鸟|掘金中]

###技术网站

###资源整合

###美工集锦

###全栈修炼

###官方文档

###前端工具

~~~~

###JS库

###JS规范

###jQuery插件及特效

###插件库

###H5 和 CSS3

###代码优化

###快速搭建项目

###深度好文

###前端搭建工具

###前端服务器

###待学习

###关于工具

###关于mac

###编辑软件

###前后端通道

###company

###微信小程序

###兼职orwork

###后端步步为营

上古利器jQuery源码剖析(一)

上古利器 虽经沧海变革 宝剑尚锋

 随着数据驱动时代的到来,jQuery不可避免的跌下神坛,但应记得它曾何等辉煌
 我们学习jQuery,不是学习它的用法,而是学习它的架构,它的思想,来深刻理解JS
 优秀的jQuery还未落伍,像当下最火着的axios/fetch等都源自对ajax的吸纳,11年就出现的异步解决方法deferred,在Promise里处处是它的影子
 之前并没有怎么看过别人的源码教程,第一次写源码稍显吃力,给自己加油吧

  * 首先,你应该知道

 $ 只是一个 jQuery的一个别名
 jQuery是一个构造函数,可以理解为就是一个普通的构造函数
 但是我们在调用jQuery的时候,并没有使用new关键字呢.其实这里,jQuery帮我们做了一层语法糖的封装,即调用$()的时候,它自动帮我们创建了jQuery的实例对象
 即$()时就为我们实现了new

  * 经典的jQuery框架

 首先我们创造一个自执行函数,传入this,通过root来接收
 在匿名函数里 我们创建一个叫jQuery的变量,jQuery变量拿到的是一个匿名函数的引用
 这个匿名函数,我们会把它当成构造函数
 因为我们是在全局环境下传入的this,所以这里的this指的就是window对象
 给环境变量root扩展一个叫做jQuery的属性,让它指向jQuery变量,同时再给root扩展一个叫做$的别名
 在随手给jQuery设置一下原型方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(function(root){
var jQuery = function () { // 构造函数

};

jQuery.prototype = {
init: function() {

}
};

//root.jQuery = jQuery;
root.$ = root.jQuery = jQuery;
})(this); // 环境变量this 因为是全局环境 所以这里指的是window

 上面的代码看起来一点都不复杂,甚至还有一点小简单,但是别忘了,我们刚才说过jQuery是一个构造函数,$()时为我们实现了new
 让jQuery返回new jQuery(),就可以完美解决!图样图破森,那样会死循环
 我们再造一个构造函数,让jQuery返回这个构造函数不就好了,这个构造函数就是init

1
2
3
4
5
6
7
8
9
10
11
12
13
(function(root){
var jQuery = function () { // 别忘了 jQuery我们也是当做构造函数的
return new jQuery.prototype.init(); // 返回构造函数init的实例对象
};

jQuery.prototype = {
init: function(){ // init是一个匿名对象,在这里我们将它当做一个构造函数

},
};

root.$ = root.jQuery = jQuery;
})(this);

  * 共享原型

 新的问题来了,$()返回了init方法,而init方法的原型对象上什么都没有,如何将方法扩展进来呢?
 如果init的原型对象是jQuery.prototype,那jQuery.prototype的所有方法都会挂载到init的实例中,也就是$()中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(function(root){
var jQuery = function () { // 别忘了 jQuery我们也是当做构造函数的
return new jQuery.prototype.init(); // 返回构造函数init的实例对象
};

jQuery.prototype = {
init: function(){ // init是一个匿名对象,在这里我们将它当做一个构造函数

},
};

jQuery.prototype.init.prototype = jQuery.prototype; // init的原型对象指向为jQuery的原型原型对象

root.$ = root.jQuery = jQuery;
})(this);

 init是jQuery.prototype的一个方法,init的原型对象又是jQuery.prototype,那么init拥有了jQuery.prototype的所有方法(包括它自己)
 设计的思路是这样的,我们有两个函数jQuery和init,$()就等同于实例化jQuery即new jQuery(),但们无法直接让jQuery函数返回new jQuery,这样会形成死循环,于是我们实例化了另一个构造函数init
 我们将init方法的原型也指向了jQuery的方法原型,实现了原型共享
 因此,jQuery.prototype原型对象里封装的所有属性和方法,jQuery与init都是可以调用的
 最反思维的地方就在于了,init是Query.prototype原型对象的一个方法,可以想想计算机的自举~

* 工具函数

 jQuery框架非常重要的一个函数-工具函数,它的作用是将jQuery所有的自身对象或者实例对象的属性和方法,全部都剥离开来,进行分类管理
 jQuery中一共有两个对象,一个是实例对象,一个是jQuery本身的对象
 起手式中我们实现了实例对象,而jQuery除了是一个构造函数,它也是一个函数对象,我们也有很多函数对象是挂载到jQuery本身上的,例如$.ajax/$.each等
 我们给jQuery添加一个静态属性fn,我们让fn等于jQuery的原型对象,这样我们在外部扩展时,至需要扩展jQuery.fn即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(function(root){
var jQuery = function () {
return new jQuery.prototype.init();
};

jQuery.fn = jQuery.prototype = { // jQuery的静态方法等于jQuery的原型对象,原型对象也是对象
init: function(){},
};

jQuery.fn.init.prototype = jQuery.fn; // 替换

root.$ = root.jQuery = jQuery;

})(this);

 外部扩展时,只需要$.fn.新方法名 = function(){} ->插件编写的形式
 通过调用静态属性扩展到jQuery的实例对象上

* extend

 extend在jQuery函数内部是作为一个工具函数存在,用它来管理不同对象(jQuery本身以及它的原型对象)的扩展
 extend在外部可以给 ①任意对象扩展/浅拷贝/深拷贝 ②jQuery本身扩展 ③jQuery实例对象扩展

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
(function(root){
var jQuery = function () {
return new jQuery.prototype.init();
};
jQuery.fn = jQuery.prototype = {
init: function(){},
};


// jQuery与 jQuery.prototype的原型都扩展了一个extend属性
jQuery.extend = jQuery.prototype.extend = function () {
// 方法调用时,我们有可能给任何对象/jQuery对象/jQuery原型对象进行扩展
// 我们通过参数来进行区分
// 参数1 是我们要扩展的对象 参数2 是我们要变量的对象
// 分析 有1个参数 可能给jQuery对象/jQuery原型对象扩展 有2个及以上 给任意对象arguments[0]扩展

var target = arguments[0] || {}; // 参数处理: 参数为空? 赋值对象不是引用类型?
var length = arguments.length; // 参数个数
var i = 1; // 默认参数个数
var option;
var name;

if(typeof target !== "object"){
target = {};
}

// 1个参数
if(length == i){

}

// 2个及以上参数
for(;i<length;i++){
if((option = arguments[i]) !== null){
for(name in option){

}
}
}
};



jQuery.fn.init.prototype = jQuery.fn;
root.$ = root.jQuery = jQuery;

})(this);



从0基础到大牛-webpack深入浅出超强完整版(四)

你可能只需要这一个教程

webpack - 昆仑山上光明顶

  loader和plugin都是webpack重要的组成部分,loader主要用于文件内的处理,而plugin主要用于文件间的处理,有时他们需要共同协作
  plugin的执行顺序与书写顺序无关,它通常是一个构造函数,挂载到webpack不同的生命周期中,等待执行

  • html-webpack-plugin

  html-webpack-plugin是我们最长的Plugin之一,它会帮我们管理html,
  下载插件npm install --save--dev html-webpack-plugin,配置如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: {},
output: {},
mode: "development",
module:{},
plugins: [

// new HtmlWebpackPlugin()
// 默认在output文件下生成一个标题为Webpack App的html文件,并引入entry内的所有文件

new HtmlWebpackPlugin({
template : "./src/index.html"
})
// 指定html模板文件 会自动复制当前文件到output文件下,并引入entry内的所有口文件
]
};

  • clean-webpack-plugin

  想要每次新打包前清除output文件,以防止废弃文件占用内存空间,我们可以使用clean-webpack-plugin
  下载插件npm install --save--dev clean-webpack-plugin,配置如下

1
2
3
4
5
6
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
plugins: [
new CleanWebpackPlugin(),
]
};

  • 指定服务器地址

  如有需要指定打包的服务器地址,可配置如下

1
2
3
4
5
6
7
module.exports = {
output: {
filename: '[name].js',
path: path.resolve(__dirname,'dist'),
publicPath: "http://xieyuxuan.cc" // 服务器地址
}
};

  打包后html引入的入口文件,都将会是publicPath的值+文件名

  • 错误调试 - sourceMap

  早期webpack打包后如有代码错误无法直接定位到源码,仅显示打包后的文件错误位置,这给开发调试造成了很大障碍
  sourceMap是一个映射关系表,能够将我们所有的源文件的映射到打包后的文件上。
  通过配置devtool,满足不同场景的调试需求
js module.exports = { devtool: 'none' // 默认值是none // source-map 会在output文件下生成一个 [output.filename].map的 映射文件 // inlne-source-map 将映射文件打入[output.filename] 内最底部 // cheap-module-source-map 生成环境中最常使用 生成[output.filename].map的 映射文件 // cheap-module-eval-source-map 开发环境中最常使用 // ... };

  • webpack-dev-server - 本地服务器

  webpack –watch命令可以实现文件更新检测,但这种方式无法满足我们的开发需要
  webpack-dev-server主要用于本地开发,满足我们的常规开发需求,大大提升我们工作效率
  它为我们开启一个本地服务器,可以实现热更新、跨域(服务器端没有跨域)、重定向等功能
  下载webpack-dev-server依赖npm install --save--dev webpack-dev-server,并在webpack.config.js中配置devServer属性

1
2
3
4
5
6
7
8
9
module.exports = {
entry: {},
output: {},
mode: "development",
module:{},
devServer:{
contentBase: './dist', // 服务器根文件地址
}
};

  在package.json中添加脚本,实现npm run dev开启本地服务

1
2
3
4
5
6
{
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "webpack-dev-server"
}
}

  终端执行npm run dev后,webpack会告诉我们webpack-dev-server为我们开启了一个地址为http://localhost:8080的服务
  此时观察dist文件,该目录下文件清空,但页面显示正常,文件更改后会自动刷新页面并重新执行
  dist下的文件不再存放在磁盘中,而是被放置在内存中

  webpack-dev-server其它常用配置如下

1
2
3
4
5
6
7
8
module.exports = {
devServer:{
contentBase: './dist', // 服务器根文件地址
open: true, // 自动打开页面
host: 'localhost', // 指定服务器地址 默认 localhost
port: 8080 // 指定端口号 默认8080
}
};

  实现HMR(热模块更新)

1
2
3
4
5
6
7
8
9
10
11
12
const webpack = require('webpack');

module.exports = {
devServer:{
contentBase: './dist',
open: true,
hot: true // 开启热模块更新
},
plugins: [
new webpack.HotModuleReplacementPlugin() // 配置热更新
]
};

  • TreeShaking

  有时我们定义了N个方法,但仅有部分方法被调用,又不想让所有的方法都被打入,这时候我们就需要shaking一下
  将未使用的 exported member 标记为 unused 同时不再导出
  这里必须使用ESmodule的引入方式
  webpack.config.js新增配置如下

1
2
3
4
5
6
7
8
module.exports = {
entry: {},
output: {},
mode: "development",
optimization: {
usedExports: true, // 开去tree shaking
},
};

  同时需要在package中新增配置,取消副作用

1
2
3
4
5
{
"name": "webpack-demo",
"version": "1.0.0",
"slideEffects": false
}

  虽然webpack设置了tree shaking,但是比如包中有一些修改、设置、日志等产生的副作用,依然会被打入
  如果你不需要这些副作用,可以设置slideEffects为false,或者通过数组指定
  我们在设置副作用时应当明确,这个包没有副作用或者不需要它的副作用
  webpack会在线上shaking掉,开发环境制作包分析

  • 开发模式 和 生产模式

  开发模式 development 本地编写代码
  生产模式 production 用于打包完成后交给线上服务
  区分两种模式,实现不同环境的打包,现我们新建webpack.dev.js做开发配置,并将所有开发环境的配置保留,可参考如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const webpack = require('webpack');


module.exports = {
entry: { 'app' : './src/app.js' },
output: {
filename: '[name].js',
path: path.resolve(__dirname,'../dist'),
},
devtool: 'cheap-module-eval-source-map',
mode: "development",
devServer:{
contentBase: './dist',
hot: true,
},
optimization:{
usedExports: true,
},
module:{}, // 此处省略
plugins: [
new HtmlWebpackPlugin(
{template : "./src/index.html"}
),
new CleanWebpackPlugin(),
new webpack.HotModuleReplacementPlugin()
]
};

  新建webpack.prod.js,做线上配置,并将所有线上环境的配置保留,可参考如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = {
entry: {'main' : './src/main.js'},
output: {
filename: '[name].js',
path: path.resolve(__dirname,'../dist'),
},
devtool: 'cheap-eval-source-map',
mode: "production",
optimization:{
usedExports: true
},
module:{}, // 此处省略
plugins: [
new HtmlWebpackPlugin(
{template : "./src/index.html"}
),
new CleanWebpackPlugin(),
]
};

  将webpack配置文件都放入根目录下的config文件夹内
  配置package.json,通过不同的命令执行不同的打包环境

1
2
3
4
5
6
7
8
{
"name": "webpack-demo",
"version": "1.0.0",
"scripts": {
"dev": "webpack-dev-server --config config/webpack.dev.js",
"build": "webpack --config config/webpack.prod.js"
}
}

  通过命令npm run dev与命令npm run build我们实现了不同环境下的打包
  我们已经可以区分环境做处理了,下一步我们进行代码优化,避免重复定义
  新建webpack.common.js做公共配置文件,并将两种环境下共同部分迁移进来,可参考如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = {
entry: { 'app' : './src/app.js' },
output: {
filename: '[name].js',
path: path.resolve(__dirname,'../dist'),
},
optimization:{
usedExports: true,
},
module:{}, // 此处省略
plugins: [
new HtmlWebpackPlugin(
{template : "./src/index.html"}
),
new CleanWebpackPlugin(),
]
};

  修改环境配置项,仅保留公共配置不包含的,可参考如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// webpack.dev.js

const webpack = require('webpack');
module.exports = {
devtool: 'cheap-module-eval-source-map',
mode: "development",
devServer:{
contentBase: './dist',
hot: true,
},
plugins: [
new webpack.HotModuleReplacementPlugin()
]
};


// webpack.prod.js
module.exports = {
devtool: 'cheap-eval-source-map',
mode: "production",
};

  如何将配置文件合并呢,我们需要webpack-merge,下载依赖npm install --save--dev webpack-merge
  通过webpack-merge返回的方法实现合并(类似于Object.assign,实现对象的合并),如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// webpack.dev.js
const webpack = require('webpack');
const merge = require('webpack-merge');
const commConfig = require('./webpack.common.js');

const devConfig = {
devtool: 'cheap-module-eval-source-map',
mode: "development",
devServer:{
contentBase: './dist',
hot: true,
},
plugins: [
new webpack.HotModuleReplacementPlugin()
]
};


module.exports = merge(commConfig,devConfig);

// webpack.prod.js

const merge = require('webpack-merge');
const commConfig = require('./webpack.common.js');

const prodConfig = {
devtool: 'cheap-eval-source-map',
mode: "production",
}

module.exports = merge(commConfig,prodConfig);

从0基础到大牛-webpack深入浅出超强完整版(三)

你可能只需要这一个教程

webpack - 万物皆可打包

  • 打包CSS

  通过import或require引入样式文件,webpack无法直接打包,需引入css-loader将其转化
  下载依赖css-loader与style-loadernpm install --save-dev css-loader style-loader,并新增配置如下

1
2
3
4
5
6
7
8
9
10
11
12

module.exports = {
module:{
rules: [
{
test: /\.css$/,
loader: ['style-loader','css-loader'],
exclude: /node_modules/
}
]
}
};

  loader的执行顺序是从后向前,即先执行css-loader,转化后交给style-loader,将其挂载到html文件内
  css-loader负责转化css,而style-loader将转化后的css,包裹在style标签内,并将其插入到html文件的header中
  此处应注意包引入的顺序与被置入的内联样式顺序有关,重复并不会转化多次
  style-loader是以文件为单位,将其包裹为一个内部样式
  css实际打包进了js文件,并未分离出来,后面将讲解css的分离配置

  • 打包CSS预编译语言(scss/less/stylus)

  css预编译语言同样无法直接编译,需要使用相应的loader,我们以sass为例
  下载sass-loader,这里我们跟着官方文档第一个来配置
  有时官方文档也会出现更新不及时的问题,另sass-loader依赖node-sass,它在底层是用C语言编写,可能会在服务器端出现依赖包问题
  下载依赖npm install sass-loader node-sass webpack --save-dev,并配置如下

1
2
3
4
5
6
7
8
9
10
11
12

module.exports = {
module:{
rules: [
{
test: /\.s[ac]ss$/i,
loader: ['style-loader','css-loader','scss-loader'],
exclude: /node_modules/
}
]
}
};

  与css相比,我们需要先使用scss-loader将sass语言转为css语言

  • 添加CSS3前缀

  浏览器一日不统一,我们就需要一日跟兼容性抗争到底。每次写样式我们都要加一堆的兼容前缀么?自动添加兼容代码,摆脱兼容烦恼
  下载依赖npm install --save--dev postcss-loader autoprefixer
  需要在css的loader中修改,我们以sass加配置

1
2
3
4
5
6
7
8
9
10
11
module.exports = {
module:{
rules: [
{
test: /\.s[ac]ss$/i,
loader: ['style-loader','css-loader','sass-loader','postcss-loader'],
exclude: /node_modules/
}
]
}
};

  同时我们需要在根目录下新建postcss的配置文件postcss.config.js,配置如下

1
2
3
modules.exports = {
plugins: [ require("autoprefixer") ]
};

  此处也可以直接配置在webpack中,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
module.exports = {
module:{
rules: [
{
test: /\.s[ac]ss$/i,
loader: ['style-loader','css-loader','sass-loader',{
loader: 'postcss-loader',
options:{
plugins: [require('autoprefixer')]
}
}],
exclude: /node_modules/
}
]
}
};

  post-loader会自动加载postcss.config.js下的配置,将文件内配置的插件加载进去
  这时再打包出来的css,将会自动为我们加入兼容前缀

  • css模块化打包

  当前引入与打包生成的css样式,是全局样式,如何做到样式隔离呢
  像vue样式的scoped,如何样式隔离只针对当前工作区呢
  css模块化配置如下(scss/less/stylus等相同)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
module.exports = {
module:{
rules: [
{
test: /\.css$/,
loader: ['style-loader',{
loader: 'css-loader',
options: {
modules: true // true/local 开启模块化机制 默认global
}
}],
exclude: /node_modules/
}
]
}
};

  之前我们引入样式的方式如下

1
import "./app.css";

  重新打包后我们发现,app.css中标签样式可以显示,而类选择器、id选择器等样式没有生效
  我们查看一下打包后生成的样式会发现样式并没有丢失,但除标签名外,其他选择器名都帮我们改为了无意义的字符串
  也就是说,如果我们想用这些样式,就要知道我们写的类名对应生成的字符串是什么,那么如何获取这些名称呢
  modules为我们抛出了一个对象,key为我们定义的选择器名,value对应生成的字符串
  我们需要这样引入:

1
2
3
4
// 个人猜测 模块化是根据原有选择器名进行编码处理 再通过export default { 选择器名1: 编码选择器名1,选择器名2: 编码选择器名2,...  }
import CSS from"./app.css";

// 假如我们在app.css有一个样式 .avatar{ ... } 此时我们只需要获取CSS.avatar 并将其绑定到对应DOM上,即可生效

  默认hash编码,我们还可以指定编码方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
module.exports = {
module:{
rules: [
{
test: /\.css$/,
loader: ['style-loader',{
loader: 'css-loader',
options: {
modules: {
localIdentName: '[path][name]__[local]--[hash:base64:5]',
},
}
}],
exclude: /node_modules/
}
]
}
};
  • 字体打包

  当我们使用外部字体库时,很显然直接引用,我们需要对字体进行打包处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module.exports = {
module:{
rules: [
{
test: /\.(woff|woff2|svg|eot|ttf)/, // 常用字体库
loader: [{
loader: 'file-loader', // 还记得我们的图片打包么
options: { // 若不配置,默认打包只output文件下,并修改文件名为编码后的字符串
outputPath: "font/", // 输出的文件路径
name: '[name].[ext]' // 输出的文件名 ext是文件后缀名称
}
}],
exclude: /node_modules/
}
]
}
};

从0基础到大牛-webpack深入浅出超强完整版(二)

你可能只需要这一个教程

webpack小试牛刀 - ES6+ 转 ES5

  • 全局垫片 polyfill

  使用babel,需要下载 babel-loader @babel/core @babel/preset-env
  babel-loader 下载 npm install -D babel-loader @babel/core @babel/preset-env webpack
  配置如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module.exports = {
module: {
rules: [
{
test: /\.js$/, // 正则 匹配规则
exclude: /node_modules/, // 依赖文件已做过处理,无需再处理,节省打包时间
loader: "babel-loader", // 使用的loader
option: { // 配置 option的配置应参照使用的loader配置列表
presets: ["@babel/preset-env" ]
}
}
]
}
};

  打包生成的文件中,ES6语法将会转为ES5语法
  但并不是所有的ES6都可以被转化,ES6中一些内置API和功能(Promise set Map)、Object.assign等无法转化
  如果想要实现上述这些方法,我们需要使用垫片库(polyfill)npm install -D @babel/polyfill
  直接引入垫片库,会将所有方法都加载进来

1
2
3
4
5

import "@babel/polyfill";
const arr = [new Promise(()=>{}),new Promise(()=>{})];

// 此时 Object.assign Set Map等方法也会被打入,生成文件骤然变大

  如何实现按需加载呢?需要配置如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
module.exports = {
module: {
rules: [
{
rule: /\.js$/,
exclude: /node_modules/,
loader: "@babel-loader",
options: {
presets: [
["@babel/preset-env",{
"useBuiltIns" : "usage",
// usage: 按需加载 页面无需引入"@babel/polyfill"
// entry: 按需加载 页面需手动引入 import "@babel/polyfill"
// 自测时发现 entry比usage打出的包更轻量

"target": { // 目标 针对浏览器版本打包
ie: "9"
}
}]
]
}
}
]
}
};

  • babel的全局配置
      webpack中options的配置可以全部放置到babel全局配置文件中,其作用和用法完全一致。首先我们先将webpack中babel的options清除
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    module.exports = {
    module: {
    rules: [
    {
    rule: /\.js$/,
    exclude: /node_modules/,
    loader: "@babel-loader",
    }
    ]
    }
    };

  根目录下新建babel配置文件: .babelrc 并配置如下(此处注意JSON写法)

1
2
3
4
5
6
7
8
{
"presets" : [["@babel/preset-env",{
"useBuiltIns": "entry",
"targets": {
"chrome": 67
}
}]]
}

  • 局部垫片 transform-runtime

  polyfill是一个全局的垫片,会将方法全部扩展到原型上。如果是开发库或者框架,就需要使用局部垫片。
  下载transform-runtime npm install --save-dev @babel/plugin-transform-runtimenpm install --save @babel/runtime
  .babelrc配置如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"presets" : ["@babel/preset-env"],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"absoluteRuntime": false,
"corejs": false,
"helpers": true,
"regenerator": true,
"useESModules": false
}
]
]
}

  transform-runtime默认按需加载,无需配置
  corejs可取值false,2,3或{ version: 2 | 3, proposals: boolean },默认为false详见文档

  • 更改文件打包地址
1
2
3
4
5
6
7
const path = require("path"); // node
module.exports = {
output:{
filename: '[name].js',
path: path.resolve(__dirname,'dist'), // __dirname为当前文件路径 dist可更改为要打包的路径名 默认dist
}
}
  • 转换TS文件

  webpack支持Typescript打包,需先下载npm install --save-dev typescript ts-loader
  根目录下新建ts配置文件:tsconfig.json 并配置如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"compilerOptions": {
"module": "es6", // 模块的引入方式 es6|commonjs
"target": "es5", // 目标语法
"allowJs": true // 是否允许JS语法
},
"include": [ // 包含文件列表
"./src/",
"./"
],
"exclude": [ // 不包含的文件
"node_modules"
]
}

  并在webpack配置中新增loader

1
2
3
4
5
6
7
8
9
10
11
module.exports = {
module: {
rules: [
{
test: /\.ts$/,
loader: "ts-loader",
exclude: /node_modules/,
}
]
}
};

  TypeSearch类型约束查找地址

  • 图片打包

  下载file-loader npm install file-loader --save-dev
  webpack中新增图片转换的loader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module.exports = {
module : {
rules : [
{
test: /\.(png|jpe?g|gif)/,
use: {
loader: "file-loader",
options:{
name: "[name]-[hash:5].[ext]", // name:图片名 ext:图片后缀名 hash:5 取Hash前5位值 默认文件名为hash:5
outputPath: "images/" // 输出文件夹(output.path)下 打包输出地址
}
},
exclude: /node_modules/
}
]
}
}

  注:loader有多种引入写法
  另一个插件url-loader包含file-loader,比file-loader更强大。npm install -D url-loader
  将上述配置file-loader改为url-loader,将不再生成image文件,会自动将图片转为base64位并注入js中。现在我们修改配置,限制转Base64大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
module.exports = {
module : {
rules : [
{
test: /\.(png|jpe?g|gif)/,
use: {
loader: "file-loader",
options:{
name: "[name]-[hash:5].[ext]",
outputPath: "images/",
limit: 1024 * 1 //1024 = 1kb 低于1kb生成base64位,以减少http请求
}
},
exclude: /node_modules/
}
]
}
}

从0基础到大牛-webpack深入浅出超强完整版(一)

你可能只需要这一个教程

webpack介绍

1.webpack发展历程

发布时间

  • webpack   v1.x   2014.02
  • webpack   v2.x   2017.01
  • webpack   v3.x   2017.06
  • webpack   v4.0   2018.05   稳定版4.32.2
  • webpack   v5.x   2019.xx   目前不是稳定的版本

模块化历程
 单页面应用(Vue React)都在用模块化

命名空间

 蛮荒时代的模块化

1
2
3
4
5
6
7
8
9
var obj = {};
obj.type = obj.type || {};
obj.type.method = function() {};
obj.type.add = function() {};
obj.type.sub = function(){};

obj.addin = obj.addin || {};
obj.addin.remove = function() {};
obj.addin.append = function() {};

 AMD与CMD的中原逐鹿

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// AMD 异步加载 依赖前置 先加载后使用
// 需要库文件 - require.js
// 定义AMD模块 文件名amd
define(function(a,b) {
'use strict';
return {
add: function(a,b) {
return a + b;
},
sub: function(a,b) {
return a - b;
}
}

});

// 加载AMD模块
require(['moduleA','moduleB','amd'],function(a,b,amd) {
console.log(a); // a模块
console.log(b); // b模块

amd.add(1,2); // 调用amd
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// CMD 同步加载 依赖后置 使用时引入 
// 需要库文件 - sea.js
// 定义CMD模块 文件名cmd
define(function(require,exports,module) {
var moduleA = require('moduleA'); // 需要时引入

exports = {
add: function(a,b) {
return a+b;
},
sub: function(a,b) {
return a - b;
}
}
});

// 加载CMD模块
seajs.use(['cmd'],function(cmd) {
cmd.add(1,2);
cmd.sub(2,3);
});

 commonJS - Node.js主要践行者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 一个文件就是一个模块 文件名common
var a = 10;
var b = 20;
function fun() {

};

// 导出方式 一
modules.export = {
a : a,
b : b,
fun : b
};

// 导出方式 二
// export.add = function(){}; ...


// 通过关键字require引入文件【相对路径/绝对路径引入】
var common = require('common');
console.log(common.a); // 10

 官方科普最硬核 - ES Module(ES6的模块化)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 使用方式 一
// 导出 文件名es1
export var a = 10;
export var add = function(a,b) {
return a + b;
}


// 引入
import { a,add } from 'es1';
add(3,5); // 调用

// 使用方式 二
// 导出 文件名es2
export default const b = {
return 20;
};

// 引入
import b from 'es2';
console.log(b);

*CSS模块化

  • OOCSS(面向对象)
  • AMCSS(属性模块)

webpack 4.32.2核心组成

  • entry : 命令行中写入口,配置文件中
  • output
  • module
  • plugins

 准备工作
  卸载旧的webpack版本 npm uninstall webpack -g
  下载最新的版本 npm install webpack -g
  检查当前webpack版本 webpack --version
  需要在本地文件内下载webpack,初始化package.json文件 npm init -y
  去下载 webpack 和 webpack-cli,3.x 的版本是不需要 webpack-cli,webpack 中集成了 webpack-cli, 4.X以后没有集成
  webpack4.x 以下的使用 webpack app.js 是可以的,4.x以后因为增加了一个多入口命令打包,需要指定出口文: webpack app.js -o bundle.js
  如果不加 –mode development 参数,默认是生产环境,会自动压缩代码
  当前可下载的webpack版本 npm info webpack webpack-5.x-bate(公测的版本)

 起手式
  根目录下新建配置文件,默认配置文件为 webpack.config.js
  你的第一个配置文件

1
2
3
4
5
6
7
8
9
module.exports = {
// 入口 相对文件地址
entry: './app.js',
// 出口
output: {
filename: 'bundle.js'
},
mode: "development"
};

  执行命令webpack后会生成 /dist/bundle.js 文件
  使用自定义名称的配置文件my-webpack-setting.js打包,请执行 webpack --config my-webpack-setting.js

  • 多入口打包

    1
    2
    3
    module.exports = {
    entry: ['app.js','app1.js','app2.js']
    }
  • 自定义出口文件名打包

    1
    2
    3
    4
    5
    6
    7
    8
    module.exports = {
    entry: {
    main: './app1.js',
    },
    output: {
    filename: '[name].js',
    }
    }

  将生成 /dist/main.js 文件

  • 多入口多出口文件打包
    1
    2
    3
    4
    5
    6
    7
    8
    9
    module.exports = {
    entry: {
    main: './app.js',
    select: './my_select.js'
    },
    output: {
    filename: '[name].js',
    }
    }

  将在dist文件夹下生成main.js与select.js
  注:多入口对应多出口,多入口对应单一出口配置时将会报错

  • ES6 module 打包
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    //  导出文件 es6.js
    export const add = function(a,b) {
    return a + b;
    }

    // 导入文件 app.js
    import { add } from './es6.js';
    console.log(add(4,7)); // 11


    // webpack配置文件
    module.exports = {
    entry: {
    app: './app.js',
    },
    output: {
    filename: '[name].js',
    },
    mode: "development"
    }

  结果生成 /dist/app.js 文件, 文件内包含 es6.js 与 app.js 代码

  • CommonJS 打包
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // 导出 common.js
    module.exports = {
    add : function(a,b) {
    return a + b;
    },
    sub: function(a,b){
    return a - b;
    }
    };

    // 导入 app.js
    const method = reqiore('./common,js');
    console.log(method.add(4,7)); // 11
    console.log(method.sub(8,3)); // 5

    // webpack配置文件 同上

  结果生成 /dist/app.js 文件, 文件内包含 common.js 与 app.js 代码

  • amd模块 打包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 导出 amd.js
define(function(require,factory) {
return {
mul: function(a,b) {
return a * b;
}
}
});

// 导入
require(['./amd.js'],function(method) {
console.log(method.mul(10,4)); // 40
});

// webpack配置文件 同上

  dist文件夹下将生成 app.js与 0.js
  0.js异步加载的内容(amd.js内容)

前端设计模式之依赖倒置原则(DIP)

前端原型的个人猜想

  相信每个学习javaScript的同僚,一定对一个说法非常熟悉『一切皆对象』.

  这个说法很好的将我们从现实社会连接进了程序世界,同时也解释了程序界很多古怪无法理解的东西.

  而事实真是这样么?在我写了这么久代码之后,我反而对javaScript最基本的东西产生了越发的迷茫和混乱.

  像隐式转换、原型链的继承这种更是完全无法想通,当我看到null和undefined时鬼知道我的脑子会爆炸出什么,最后扭合到一起,变成一堆的问号…

  现在我希望彻底能搞清祖师爷在创造javaScript是究竟是怎样的思路,这对我来说很难,但我知道当我搞清楚这个问题时,会有一个很大的飞跃,我不应该继续『不了解但不影响工作』在的舒适区混下去.

  感谢jQuery的源码,让我有了共享原型的概念,不然我一定会强制要求原型链必须是个有唯一顶点的树状结构。

  *请注意下面代码中,一定要将Object.prototype和Function.prototype当成是独立的一部分

1
Object.prototype.__proto__ == null   // true

  这里我们知道了Object.prototype的原型链向上指向是null,注意我没有说它继承自null,因为null是个很特别的类型,我们都知道原型的本质是个对象,所以它应该继承自对象.
  那这里怎么会是个null呢?我是猜想浏览器帮我们指向了null.
  太好了,至少有一个封闭的点了!我们继续.

1
Object.__proto__ == Function.prototype  // true

  大声尖叫,Object竟然继承自Function.prototype,『一切皆对象』并不是最后的真像!就好比我们好不容易发现了真菌细菌这些肉眼看不见的小东西,转眼我们又发现了分子原子,难道大世界无限的在嵌套我们不知道的小世界么?

  好在程序世界的构成元素就是这几个,根本不存在宇宙这种无休无止的构成一说,但程序世界与现实世界还有一个最大的区别,叫做『自举』,这里我们可以简单理解为『我可以创造我自己』.

  有了上面代码,就下面代码的成立.

1
Object instanceof Function == true

  但是我们还知道

1
Object instanceof Object == true

  这里是『我创造了我自己』么?不是的,这里完全是多绕了一步

1
2
Object.__proto__ == Function.prototype   // true
Function.prototype.__proto__ == Object.prototype // true

  目前还没有出现Object,我们又回到了Object.prototype,而Object.prototype.proto等于null这件事我们在第一步就已经证实过了.

  问题出在哪呢?当然还是Object.prototype的继承问题!刚刚说过了Object.prototype很有可能是Object的实例,并保留了其继承关系,而浏览器对其赋值为null.

  这完全出自我的个人想法,因为如果不是这样,这条原型链的解释则都不成立.

  再看这个

1
Function.__proto__ == Function.prototype  // true

  Function继承自自己

/* 这里非常明确了Object继承自Function.prototype */


/*我们知道了上面代码的关系 下面这句就理所当然了*/


/* 但我们还知道*/

webpack4.X 小白从0开始一步学到位

来自三刷老肖的学习视频,依个人理解有增删

webpack介绍

1.webpack是什么
  • 打包工具(模块)
  • 前端必不可少的工具
2. webpack的作用
  • 打包(把多个文件打包成一个js文件,减少服务器压力、带宽)
  • 转化(比如less/sass/ts)需要loader
  • 优化(SPA越来越盛行,前端项目复杂度高,webpack可以对项目进行优化)
3. webpack构成
  • 入口 (entry)
  • 出口 (output)
  • 转化器 (loader)
  • 插件 (plugins)
  • 开发服务器 (devServer)
  • 模式 (mode)
4. 安装webpack
1
2
3
4
5
npm install webpack-cli -g
// or
yarn add webpack-cli -g
// 验证webpack环境已经OK ?
webpack -v
5. 开发环境和生产环境
  • 开发环境(development): 就是你平时编写代码的环境
  • 生产环境(production): 项目开发完毕,部署上线
6. npm基本命令
  • 生成package.json:
    • npm init
    • npm init -y
  • 下载线上环境包依赖
    • npm install [包名,包名,包名…] –save
    • npm i [包名,包名,包名…] -S
  • 下载开发环境包依赖
    • npm install [包名,包名,包名…] –save–dev
    • npm i [包名,包名,包名…] -D

webpack4.x初探

1. 跑一跑webpack

  当前项目下,新建src文件,src文件下新建index.js
  index.js文件编写测试代码【console.log(‘Hello World’);】
  将src下index.js打包为dist下bundle.js,执行如下:

1
webpack src/index.js --output dist/bundle.js
2. webpack.config.js配置文件

  当前项目下执行npm init -y
  当前项目下新增webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
const path = require('path');
module.exports = {
entry: {
app: './src/index.js'
},
output:{
filename: 'bundle.js',
path: path.resolve(__dirname,'/dist')
},
module: {},
plugins: [],
devServer:{},
};

3. 使用自定义webpack配置文件名 例:myself.config.js
1
webpack --config myself.config.js
4. package.json配置运行命令

  package.json文件scripts下配置命令
  配置webpack开发环境(dev)/线上环境(build)命令如下

1
2
3
4
"scripts": {
"dev": "webpack --mode development",
"build": "webpack --mode production",
},

  执行开发环境打包:npm run dev
  执行线上环境打包:npm run build
  mode:设置打包环境

多入口、多出口、html插件使用

1. 多入口打包
1
2
3
4
5
6
7
module.exports = {
entry: ['./src/index.js','./src/index2.js'],
output:{
filename: 'bundle.js',
path: path.resolve(__dirname,'dist')
},
};
2. 多出口
1
2
3
4
5
6
7
8
9
10
module.exports = {
entry: {
app: './src/index',
main: './src/index2.js'
},
output:{
filename: '[name].bundle.js',
path: path.resolve(__dirname,'dist')
}
};
3. html-webpack-plugin

  作用:生成页面
  依赖webpack webpack-cli
  安装插件:npm i html-webpack-plugin -D
  使用如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
...,
plugins:[
new HtmlWebpackPlugin({
template: './src/index.html',
title: '可配置标题',
hash: true,
minify:{
collapseWhitespace: true
},
})
]
};

1
2
3
4
5
<head>
<meta charset="UTF-8">
<title><%= htmlWebpackPlugin.options.title%></title>
</head>
<!--htmlWebpackPlugin 为插件定义名称,与js中自定义的插件名无关 -->

参数解析

  • title:HTML文档标题
  • filename: 文件名,默认为index.html,可设置
  • template: 版本地址,默认查询 src/index.ejs,若未查到,将自动生成模板
  • templateParameters: 允许重新模板参数
  • inject: 注入,允许对head、body等添加脚本
  • favicon: 设置网页顶部图标
  • meta: 在head中注入mate标签
  • base: 设置公共地址,如 https://example.com/path/page.html
  • minify: 是否压缩
  • hash: 是否生成哈希值,若为true将为所以注入文件加入哈希值
  • cache: 是否启用缓存,仅在文件有改动后才更新文件
  • showErrors: 是否报错
  • chunks: 是否仅允许添加指定块
  • chunksSortMode: 设置块加载排序方式
  • excludeChunks: 不允许加入的块
  • xhtml: 是否遵照xhtml加入link标签

HTML插件优化、服务器、热更新

1. 生成多页面
1
2
3
4
5
6
7
8
9
10
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
}),
new HtmlWebpackPlugin({
filename: 'index2.html',
template: './src/index2.html',
}),
...
]

  配置多个页面时,此处一定要配置filename
  mpa项目可用node读取文件,来生成多个模板
  默认js都会打入模板内

2. 指定文件打入指定模板

  将app文件打入index.html,将main文件打入index2.html,配置如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module.exports = {
entry: {
app: './src/index',
main: './src/index2.js'
},
plugins: [
new HtmlWebpackPlugin({
chunks:['app'],
template: './src/index.html',
}),
new HtmlWebpackPlugin({
chunks:['main'],
filename: 'index2.html',
template: './src/index2.html',
})
],
};

3. clean-webpack-plugin

  作用:清除文件/文件夹
  使用方式:

1
2
3
4
5
6
7
8
const {CleanWebpackPlugin} = require('clean-webpack-plugin');
module.exports = {
...,
plugins: [
new CleanWebpackPlugin(),
],
devServer:{},
};

4. devServer

  下载: npm install webpack-dev-server
  package.json中配置启动项

1
2
3
4
5
"scripts": {
"dev": "webpack --mode development",
"build": "webpack --mode production",
"start:dev": "webpack-dev-server"
},

  配置一个基本的devServer

1
2
3
4
5
6
7
8
9
10
11
module.exports = {
...,
devServer:{
// 设置服务器访问的基本目录
contentBase: path.resolve(__dirname,'dist'),
// 服务IP地址,本地可设置 localhost / 127.0.0.1
host: 'localhost',
// 设置端口
port: '8090',
},
};

  项目中我们发现dist文件为空,因为devServer将contentBase下的文件放到了活动内存中
  devServer支持自动刷新

5. 热更新

  作用:不重新加载整个文档,保留之前的操作,仅对有变动的部分更改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const webpack = require('webpack');

module.exports = {
...,
devServer:{
// 设置服务器访问的基本目录
contentBase: path.resolve(__dirname,'dist'),
// 服务IP地址,本地可设置 localhost / 127.0.0.1
host: 'localhost',
// 设置端口
port: '8090',
// 是否开启热更新
},
plugins: [
new webpack.HotModuleReplacementPlugin()
]
};

6, 实现跨域

  利用devServer实现跨域
  将所有/api开头的请求,代理至本机3000端口,配置如下

1
2
3
4
5
6
7
8
devServer: {
proxy: {
'/api': {
target: 'http://localhost:3000',
pathRewrite: {'^/api' : ''}
}
}
}

  pathRewrite:重写

loader作用,处理css文件及压缩方式

1. loader介绍

  作用:加载器、转化器
  比如:less/scss转成css;ES7/8/9、JSX转成js
  loader从后向前加载

2. css loader

  我们编写了一个app.css文件,并在js文件中引用了它

1
import "./app.css";

  此时工具将会报编译错误,提示我们缺少loader,下面我是使用loader将其转化
  下载css相关的两个最核心loader:style-loader与css-loader
  loader基础配置如下:

1
2
3
4
5
6
7
8
9
10
11
module.exports = {
module: {
rules: [
{
test: /\.css$/,
loader:['style-loader','css-loader'],
exclude: /node_modules/
}
]
},
}

关于loader的写法

  • loader: [‘loader1’,’loader2’,…]
  • use: [‘loader1’,’loader2’,…]
  • use: [{loader: ‘loader1’},{ loader: ‘loader2’ },…]

css-loader先于style-loader解析
css-loader用来解析文件,style-loader将注入到模板内(在模板的head标签内注入style标签,并将样式放入style标签内)

3. 文件压缩

  4.x版本之前,压缩插件使用uglifyjs-webpack-plugin
  4.x版本之后,仅需将模式改为线上模式,将自动生成压缩文件

处理图片,分离css

1. 图片处理

  当我们在css中引用一张路径正确的图片时,编译工具会报错(当时直接引入css的时候也是这样)
  图片转化我们经常会用到两个loader: url-loader和file-loader
  url-loader: 支持js引入文件,并将文件输出到output的文件夹
  file-loader与url-loader相似,但比url-loader功能还要强大
  url-loader使用如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module: {
rules: [
{
test: /\.(png|jpg|gif|jpeg)$/i,
use: {
loader: 'url-loader',
options: {
limit: 50*1000,
outputPath: 'image',
}
},
exclude: /node_modules/
}
]
},

  背景图小于50kb时,图片会生成base64位

2. 图片分离

  图片分离插件 extract-text-webpack-plugin 4.0版后已废弃
  4.0版后请使用mini-css-extract-plugin
  使用如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
module: {
rules: [
{
test: /\.css$/,
loader:[{
loader: MiniCssExtractPlugin.loader,
options:{
publicPath: ''
}
},'css-loader'],
exclude: /node_modules/
},{
test: /\.(png|jpg|gif|jpeg)$/i,
use: {
loader: 'url-loader',
options: {
outputPath: 'image',
limit: 50*1000
}
},
exclude: /node_modules/
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].css',
chunkFilename: '[id].css'
}),

],
}

less/sass处理,添加前缀,消除冗余css

1. 处理less

  下载转化less的loader: npm install less less-loader -D
  配置如下:

1
2
3
4
5
6
7
8
9
module: {
rules: [
{
test: /\.less$/,
use: ['style-loader','css-loader','less-loader'],
exclude: /node_modules/,
}
]
},

2. 处理sass

  下载转化sass的loader: npm install node-sass sass-loader -D
  配置如下:

1
2
3
4
5
6
7
8
9
module: {
rules: [
{
test: /\.s(a|c)ss/,
use: ['style-loader','css-loader','sass-loader'],
exclude: /node_modules/
}
]
},

3. 自动添加css前缀

  CSS预处理器: postCss
  下载预处理器loader:npm install postcss-loader autoprefixer -D
  项目根目录下新增postcss配置文件: postcss.config.js,配置如下

1
2
3
4
5
module.exports = {
plugins: [
require('autoprefixer')
]
}

  webpack配置如下

1
2
3
4
5
6
7
8
9
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader','css-loader','postcss-loader'],
exclude: /node_modules/
}
]
},

  目前遇到项目中不显示前缀,但demo中就可以,还在排查原因

4. 消除冗余CSS代码

  下载依赖:npm install purifycss-webpack purify-css -D
  paths参数用来设置需要分析的路径地址
  配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
   
const path = require('path');
const glob = require('glob');
const PurifyCssPlugin = require('purifycss-webpack');

module.exports = {
plugins: [
new PurifyCssPlugin({
paths: glob.sync(path.join(__dirname, 'dist/*.html'))
})
]
}

SourceMap、babel、react环境配置

1. SourceMap

  webpack 4.x版本默认在development环境下自动开启调试模式
  webpack 3.采用sourceMap

1
2
3
4
module: {
...,
devTool: 'source-map'
},
2. babel

作用:

  • babel用来编译js
  • 能够轻松使用ESnext转化
  • 支持jsx等语法

  babel核心模块: babel-loader @babel/core @babel/preset-env
  配置如下:

1
2
3
4
5
6
7
8
9
10
11
module: {
rules: [
test: /\.jsx?$/,
exclude: /node_modules/,
use: [{
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}]
]
}
3. 支持JSX语法

  下载插件:npm install react react-dom @babel/preset-react -D
  项目根目录下新建babel配置文件.babelrc.json

1
2
3
4
5
6
7
8
{
"presets": ["@babel/preset-react"],
"env": {
"development": {
"presets": [["@babel/preset-react", { "development": true }]]
}
}
};

JSON配置、模块化设置、静态资源、插件

1. 使用模块化

  以提取rules为例
  新建rules模块文件webpack.rule.js
  复制模块规则并通过module.exports导出
  webpack配置文件引入如下

1
2
3
4
5
6
const ruleConfig = require('./webpack.rule');
module.exports = {
module: {
rules: ruleConfig
},
}

  在webpack中使用JSON

  • webpack 3.X之前使用json-loader
  • webpack 3.X之后默认可自动识别
2. 静态资源输出

  使用插件copy-webpack-plugin
  配置如下

1
2
3
4
5
6
7
8
const CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = {
plugins: [
new CopyWebpackPlugin([
{from : path.resolve(__dirname,'src/assets'),to: './assets'},
])
]
}

  注:默认打入dist文件

优雅使用第三方库、提取JS文件、优化

1. 使用第三方库

   ① 通过npm引入
   ② 通过webpack内置插件ProvidePlugin,向全局暴露,使用方法如下

1
2
3
4
5
6
7
8
const webpack =  require('webpack');
module.exports = {
plugins: [
new webpack.ProvidePlugin({
$: 'jquery',
})
]
}

  通过ProvidePlugin引入与通过import引入的区别:

  • 通过import引入后,无论是否使用,都会被打包,这样会产生大量冗余js
  • ProvidePlugin只有在使用后才会打包
2. 提取JS文件

  在webpack 3.x及之前使用CommonsChunkPlugin
  webpack 4.x使用SplitChunksPlugin,使用如下

1
2
3
4
5
6
7
8
const webpack = require('webpack');
module.exports = {
optimization: {
splitChunks: {

}
}
}

  最后,这套课程老肖录制于18年初,截至到现在已经有一些插件、loader或写法发生了变化,我已对其做了少量调整,即便如此,对于新手而言这一定是一个很棒的课。
  这套课使自己快速入了webpack的门,每看一遍都有新收获,后面我将再写一篇由浅入深更全面、更深入的教程课,但老肖这套,唯有纸笔记之算自己才觉得可以算完结,好在我做到了。