My Little World

learn and share


  • 首页

  • 分类

  • 标签

  • 归档

  • 关于
My Little World

vue 源码学习一【new vue】

发表于 2020-01-27

new vue时发生了什么

平常练习时使用vue顺序是,将vue.js引入html,
new vue就可以开始使用vue框架写东西

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<body>
<div id = 'app'>
<p>{{a}}</p>
</div>
<script type="text/javascript" src="vue.js"></script>
<script>
let app = new Vue({
el:'#app',
data:{
a:'hello world'
},
method:{},
})
</script>
</body>

代码运行顺序是,当我们在script标签引入文件时,
引入的vuejs文件会先执行一遍,
执行的结果就是帮助我们生成一个挂载在全局的,vue对象的构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = global || self, global.Vue = factory());//将vue挂到全局上下文中
}(this, function () { 'use strict';
function Vue (options) {
if (!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword');
}
this._init(options);
}
initMixin(Vue);
stateMixin(Vue);
eventsMixin(Vue);
lifecycleMixin(Vue);
renderMixin(Vue);
initGlobalAPI(Vue);
Vue.compile = compileToFunctions;

return Vue;

}));

当代码运行到new vue时,代码就会调用function Vue
function Vue 中会【调用】自己的_init()方法,
对即将生成的vue对象做初始化工作,即开始生成vue对象

_init方法定义在initMixin函数中
在调用initMixin生成构造函数流程中被挂到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
35
36
Vue.prototype._init = function (options) {
var vm = this;
vm._uid = uid$3++;

var startTag, endTag;
vm._isVue = true;// a flag to avoid this being observed
// 合并选项,将配置项以内置选项进行合并
if (options && options._isComponent) {
//优化内部组件实例化过程
// 由于动态选项合并非常慢,并且内部组件选项均不需要特殊处理。
initInternalComponent(vm, options);
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
);
}
/* istanbul ignore else */
{
initProxy(vm);
}
// expose real self
vm._self = vm;
initLifecycle(vm);
initEvents(vm);
initRender(vm);
callHook(vm, 'beforeCreate');//运行订阅了beforecreate钩子的相关方法
initInjections(vm); // resolve injections before data/props
initState(vm);
initProvide(vm); // resolve provide after data/props
callHook(vm, 'created');
if (vm.$options.el) {//如果配置了el,就进行挂载处理挂载
vm.$mount(vm.$options.el);//会给给option挂上render,staticRenderFns属性
}
};

My Little World

一些关于vue的零散笔记

发表于 2020-01-27

释放内部对象且能防止外部修改的方式

通过创建中间对象挂载到实例上,通过中间对象的set方法进行拦截

1
2
3
4
5
6
7
8
9
10
11
12
function initGlobalAPI (Vue) {
var configDef = {};
configDef.get = function () { return config; }; //config对象是vue对象构建过程的内部对象,存储全局配置项
{
configDef.set = function () {
warn(
'Do not replace the Vue.config object, set individual fields instead.'
);
};
}
Object.defineProperty(Vue, 'config', configDef);
}

Vue.config.optionMergeStrategies 应用中
Vue.config可以get,得到的即内部的config,但如果进行Vue.config = xxxx set操作就会报错
通过Vue.config可以获得内部config,从而可以修改内部config对象上的属性,从而修改到全局配置

关于3.0的思考

如何更快
Object.defineProperty–>Proxy
Virtual Dom重构
更多编译优化:
Slot默认编译为函数
重构vnode factory,统一输入参数,进一步优化编译处理函数(Monomorphic vnode factory)
编译时增加vnode/children 类型信息标识,从而做进一步优化(Compiler-generated flags for vnode/children types)

Function based API 对比Class API 好处
更好的TS类型推导支持
更灵活的逻辑复用能力
Tree-shaking 友好
代码更容易压缩

现有逻辑复用模式
Mixins
高阶组件
基于scoped slots/作用域插槽封装逻辑的组件
存在问题
数据来源不清晰 (Function based API 数据来源清晰)
命名冲突(Function based API 没有命名冲突)
无谓的额外组件的性能消耗 (Function based API 没有额外组件性能消耗)

对比React Hooks
同样的逻辑组合,复用能力更强
只调用一次
符合JS直觉
没有闭包变量问题
没有内存/GC压力
不存在内联回调导致子组件永远更新问题

2.0
mergeOption 选项策略处理
插件开发:以默认配置优先;以用户配置为覆盖
策略:听过JS 增强配置选项的表达力
{}==>function(){} el===根实例

数据劫持vue3.0改用proxy原因
defineProperty只能监听某个属性,不能对全对象监听,使用proxy可以省去for in提升效率
可以监听数组,不用再去单独的对数组做特异性操作

vue2.0 vdom性能瓶颈
虽然能够保证触发更新的组件最小化,但在单个组件内部依然需要遍历该组件的整个Vdom树
vdom的性能跟模板大小正相关,跟动态节点的数量无关。
在一些组件整个模板内只有少量动态节点的情况下,这些遍历都是性能的浪费

编译3步骤

parse:AST
optimize:标记静态节点
generate 生成目标平台所需的代码 将AST转化成render function字符串

修饰符

v-model.lazy 从input事件中转变为在change事件中同步数据
v-model.number 可以将输入转换为Number类型
v-model.trim 可以自动过滤输入的首尾空格

v-for 循环时,key尽量赋值为不变的值,即静态值,如item.id ,否则列表增删时,会更新每一项

当子组件需要向父组件传递数据时,就要用到自定义事件
子组件用$emit来触发事件,父组件用$on()来监听子组件的事件
自定义组件的v-model :一个组件上的v-model 默认会利用名为value的prop和名为input的事件
将原生事件绑定到组件: 使用v-on的.native修饰符监听原生事件
.sync修饰符:父组件监听自定义事件按需更新数据

组件

动态组件:VUE 提供了一个特殊的元素<component>用来动态的挂载不同的组件,使用is特性来选择要挂载的组件, 还可以使用<keep-alive>标签使组件进行缓存

异步组件:VUE允许将组件定义为一个工厂函数,动态地解析组件。VUE只在组件需要渲染时触发工厂函数,并且把结果缓存起来用于后面地再次渲染

组件创建方式

1.调用Vue.extend(),创建名为xx的组件,template定义模板的标签,模板的内容需要写在该标签下

1
2
3
var xx = Vue.extend({
template:'<div>this is a component</div>'
})

2.使用<template>标签创建,需要加上id属性

1
2
3
<template id = 'mycom'>
<div>this is a component</div>
</template>

3.使用<script>标签创建,需要加id属性,同时还得加type=’text/x-template’ 不执行编译里面的代码

1
2
3
<script type='text/x-template' id = 'mycom'>
<div>this is a component</div>
</script>

组件全局注册方式

1.调用Vue.extend(),创建名为myCom的组件全局注册

1
Vue.component('my-com',myCom)

2.template及script标签构建的组件全局注册

1
2
3
Vue.component('my-comp',{
template:'#mycom'
})

组件局部注册

1.调用Vue.extend(),创建名为myCom的组件局部注册(只能在注册该组件的实例中使用,一处注册,一处使用)

1
2
3
4
5
6
var app = new Vue({
el:'#app',
components:{
'my-com':myCom
}
})

2.template及script构建的组件局部注册

1
2
3
4
5
6
7
8
var app = new Vue({
el:'#app'
componnets:{
'my-com':{
template:'#myCom'
}
}
})

组件化处理边界情况

1.访问根实例 this.$root.xxx
2.访问父组件实例:this.$parent.xxx
3.访问子组件实例或子元素:<child ref=’xxx’></child > =>this.$refs.xxx
4.依赖注入:

1
2
3
4
5
6
provide:function(){ //父组件
return{
getMap:this.getMap
}
}
inject:[getMap]//子组件

然而,依赖注入还是有负面影响的。它将你应用程序中的组件与它们当前的组织方式耦合起来,使重构变得更加困难。同时所提供的属性是非响应式的。这是出于设计的考虑,因为使用它们来创建一个中心化规模化的数据跟使用 $root做这件事都是不够好的。如果你想要共享的这个属性是你的应用特有的,而不是通用化的,或者如果你想在祖先组件中更新所提供的数据,那么这意味着你可能需要换用一个像 Vuex 这样真正的状态管理方案了

5.统一处理事件侦听器时,在组件卸载前,统一清除监听器
6.组件循环引用,Vue.component注册的组件允许,但是模块化导入的不允许
7.模板定义的替代品:
A:内联模板:当inline-template这个特殊的特性出现在一个子组件上时,这个组件将会使用其里面的内容作为模板
而不是将其作为被分发的内容

1
<myCom inline-template><div><p>these are compiled as the component's own template</p></div></myCom>

B:x-template
在script标签里使用text/x-template,并且指定id,将这个id赋值给template

1
2
3
4
5
6
<script type='text/x-template' id = 'mycom'>
<div>this is a component</div>
</script>
Vue.component('my-comp',{
template:'#mycom'
})

8.强制更新
A:使用this.$forceUpdate
B:使用v-once创建低开销静态组件,组件包含大量静态内容可以在根元素上添加v-once特性以确保这些内容只计算一次然后缓存起来

组件通信类型

父子组件通信
1.使用props和$emit父子组件相互通信
2.父组件用$children或者利用ref操作子组件
3.子组件$parent访问父组件

非父子组件通信
1.使用中央事件总线(eventbus来处理非父子组件间的通信)
2.祖先元素通过provide提供数据,后代通过inject获取该数据
3.使用$attrs和$listeners实现祖孙组件通信
4.$root直接访问根组件

Vue.use

Vue.use(myPlugin)本质上是调用myPlugin.install(VUE)
使用插件必须在new vue()启动应用之前完成,实例化之前就要配置好
使用Vue.use多次注册相同插件,那只会注册成功一次
一个测试案例

1
2
3
4
5
6
7
8
9
10
11
12
13
import Vue from 'vue'
import {expect} from 'chai'
import Counter from '@/counter.vue'

describe('测试Counter.vue',()=>{
const Constructor = Vue.extend(Counter)
const vm = new Constructor().$mount()
const button = vm.$el.querySelector('button')
const clickE = new window.Event('click')
button.dispatchEvent(clickE)
vm._watcher.run()
expect(Number(vm.$el.querySelector('span').textContent)).to.equal(1)
})

vuex

默认的五种基本对象
state:存储状态(对象)
getters:对数据获取之前的再次编译,可以理解为state的计算属性,对state的数据进行筛选,过滤
mutations:同步修改状态,并且是同步的。在组件中使用$store.commit(‘’,params)
actions:异步修改状态,在组件中使用是$store.dispatch(‘’)
modules:store的子模块,为了开发大型项目,方便状态管理而使用的

My Little World

一些关于H5的知识

发表于 2020-01-27

h5 新增语义化布局标签:header,nav,section,aside,article,footer,均表现为块级
canvas,video,audio,sessionStorage,localStorage,
拖放API(源对象:ondragstart,ondrag,ondragend;源对象标签属性要设置为draggable=true
目标对象:ondragenter,ondragover,ondragleave,ondrop;
ondragover事件默认会屏蔽ondrop事件,
如果要触发ondrop事件,需要在ondragover事件函数中阻止默认屏蔽行为
event.preventDefault()
)
//谷歌浏览器v56 之后,window,document,body的touchstart,touchmove事件会被默认为pssive:true
//解决方法一:window.addEventListener(‘touchstart’,func,{passive:false})
//解决方法二,添加样式:*{touch-action:none} 取消所有元素默认事件

1
2
3
4
5
6
7
8
9
10
//实现把A标签拖到B标签
1.A标签开始拖动时
ondragstart(event){
event.dataTransfer.setData('source',event.target.id)
}
2.B标签监听拖拽结束后
ondrop(event){
var source = document.getElementById(event.dataTransfer.getData('source'))
event.target.appendChild(source)
}

History常用场景
单页应用中:实现网页无刷新更新数据的同时,解决浏览器无法前进/后退的问题
pushState:每执行一次都会增加一条历史记录,浏览器在返回时,就不会返回前一个页面,并且不会刷新浏览器
replaceState:用来修改当前历史记录,而不是创建一个新的历史记录,点击返回按钮照样会返回上一个页面
onpopstate:点击后退,前进或者调用history.back(),history.forward(),history.go()方法

跨文档通讯

定义:跨文档消息传送,来自不同域的页面间传递消息
使用场景:内嵌框架和其父文档相互进行数据传输或者通讯

postMessage

window.postMessage(message,origin,transfer)
message:发送到子文档的信息,一般转成字符串,否则可能出现浏览器不兼容
origin:域信息,告诉子文档,消息来源,用于判断来源是否匹配,然后才进行相关操作
transfer:转移消息对象,可选参数,是一串和message同时传递的Transferable对象,这些对象的所有权将被转移给消息的接收方,而发送方将不再保有所有权

onmessage

onmessage事件回调函数参数event事件对象
event.data:postMessage传输过来的第一个参数,通常是字符串类型,(也可以是其他类型,但可能出现浏览器不兼容情况,所以一般转成字符串传递)
event.origin:postMessage传输过来的第二个参数,是用来匹配来源方的域
event.source:来源方目标文档的window引用,通常用作单次握手回应数据(event.source.postMessage())
event.ports:MessageEvent接口特有属性,用于获取消息端口集合对象

实现

父文档A通过iframe内嵌框架加载B子文档
通过iframe内嵌框架的onload事件回调父文档的sendMsg函数
再sendMsg函数中通过postMessage()函数向子文档B发送消息

子文档B监听onmessage事件,当文档收到消息后会执行该事件回调函数
回调函数内通过event对象判断域是否安全,然后处理推送过来的消息,
可以再通过调用event.source.postMessage向父文档发送消息

地理位置

navigator.geolocation.getCurrentPosition(success,error,option)
success:成功得到位置时的回调函数,使用Position对象作为唯一的参数
error:失败时回调,使用PositionError对象作为唯一参数
options:可选参数,对请求做一些配置

离线存储优势

离线浏览:当用户网络断开时,可以继续访问页面
访问速度快:将资源缓存到本地,已缓存资源加载更快
减少服务器负载:浏览器将只会从服务器下载更新过或者更改过的资源

视频播放兼容

Flv.js

H5 Flash(FLV)播放器,纯原生js开发,使H5能够支持FLV格式的视频
原理:将FLV文件流转码复用为ISO BMFF(MP4碎片)片段,然后通过Media Source Extension将MP4片段汇进浏览器
使用es6编写,通过Babel Compiler编译成es5能够支持FLV格式的视频

Video.js

几乎兼容所有浏览器,且优先使用H5,在不支持的浏览器中自动使用Flash播放
构建视频播放页面耗时短,界面可定制,开源,纯JS和css实现,文档详细

#Canvas绘制动画步骤
1.清空canvas
除非接下来要画的内容会完全充满canvas(例如背景图),否则需要清空所有,最简单
的做法就是clearRect方法
2.保存canvas状态
如果需要改变一些会改变canvas状态的设置(样式,变形之类的),又要在每画一帧之时,都是原始状态的话,需要先保存一下
3.绘制动画图形
重绘动画帧
4.恢复canvas状态
如果已经保存了canvas的状态,可以先恢复它,然后重绘下一帧

transform

2D功能函数组合使用时,先写的后执行
perspective:100px 可以设置3d旋转时的景深
transform-origin:可以改变中心点

响应式布局

可以通过link标签配置media去使用不同比例的CSS样式

1
2
<link rel='stylesheet' media='screen and (max-device-width:480px)' href='./css1.css'>
<link rel='stylesheet' media='screen and (min-device-width:480px)' href='./css2.css'>

一些触摸事件的实现

My Little World

一些vueRouter官方文档看到的

发表于 2019-11-28

复用组件

当使用路由参数时,例如从 /user/foo 导航到 /user/bar,原来的组件实例会被复用。
因为两个路由都渲染同个组件,比起销毁再创建,复用则显得更加高效。不过,这也意味着组件的生命周期钩子不会再被调用。

复用组件时,想对路由参数的变化作出响应的话,可以简单地 watch (监测变化) $route 对象

参数或查询的改变并不会触发进入/离开的导航守卫。
可以通过观察 $route 对象来应对这些变化,或使用 beforeRouteUpdate 的组件内守卫。

参数组合

如果提供了 path,params 会被忽略,query 并不会被忽略。你需要提供路由的 name 或手写完整的带有参数的 path
path—–query name—–params

beforeRouteUpdate

如果目的地和当前路由相同,只有参数发生了改变 (比如从一个用户资料到另一个 /users/1 -> /users/2),你需要使用 beforeRouteUpdate 来响应这个变化 (比如抓取用户信息)

router-view标签

没有设置名字,那么默认为 default;如果设置了name,那么该视图显示路由中components中配置的name值对应的组件

重定向

重定向三种形式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
1.
const router = new VueRouter({
routes: [
{ path: '/a', redirect: '/b' }
]
})

2.命名的路由
const router = new VueRouter({
routes: [
{ path: '/a', redirect: { name: 'foo' }}
]
})
3.方法,动态返回重定向目标:
const router = new VueRouter({
routes: [
{ path: '/a', redirect: to => {
// 方法接收 目标路由 作为参数
// return 重定向的 字符串路径/路径对象
}}
]
})

导航守卫并没有应用在跳转路由上,而仅仅应用在其目标上,为 /a 路由添加一个 beforeEach 或 beforeLeave 守卫并不会有任何效果

“重定向”的意思是,当用户访问 /a时,URL 将会被替换成 /b,然后匹配路由为 /b,
/a 的别名是 /b,意味着,当用户访问 /b 时,URL 会保持为 /b,但是路由匹配则为 /a,就像用户访问 /a 一样。

1
2
3
4
5
const router = new VueRouter({
routes: [
{ path: '/a', component: A, alias: '/b' }
]
})

props

通过配置props属性实现多路由复用组件

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
1.如果 props 被设置为 true,route.params 将会被设置为组件属性。
const User = {
props: ['id'],
template: '<div>User {{ id }}</div>'
}
const router = new VueRouter({
routes: [
{ path: '/user/:id', component: User, props: true },

// 对于包含命名视图的路由,你必须分别为每个命名视图添加 `props` 选项:
{
path: '/user/:id',
components: { default: User, sidebar: Sidebar },
props: { default: true, sidebar: false }
}
]
})

2.如果 props 是一个对象,它会被按原样设置为组件属性。当 props 是静态的时候有用
const router = new VueRouter({
routes: [
{ path: '/promotion/from-newsletter', component: Promotion, props: { newsletterPopup: false } }
]
})

3.创建一个函数返回 props
const router = new VueRouter({
routes: [
{ path: '/search', component: SearchUser, props: (route) => ({ query: route.query.q }) }
]
})
URL /search?q=vue 会将 {query: 'vue'} 作为属性传递给 SearchUser 组件

请尽可能保持 props 函数为无状态的,因为它只会在路由发生变化时起作用。如果你需要状态来定义 props,请使用包装组件,这样 Vue 才可以对状态变化做出反应

路由生命周期

全局配置

router.beforeEach:当一个导航触发时,全局前置守卫按照创建顺序调用。守卫是异步解析执行,此时导航在所有守卫 resolve 完之前一直处于 等待中

1
2
3
4
5
6
7
8
三个参数
to: 即将要进入的目标 路由对象
from: 当前导航正要离开的路由的路由对象
next:一定要调用该方法来 resolve 这个钩子。执行效果依赖 next 方法的调用参数
next(): 进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed (确认的)。
next(false): 中断当前的导航。如果浏览器的 URL 改变了 (可能是用户手动或者浏览器后退按钮),那么 URL 地址会重置到 from 路由对应的地址。
next('/') 或者 next({ path: '/' }): 跳转到一个不同的地址。当前的导航被中断,然后进行一个新的导航。
next(error): 如果传入 next 的参数是一个 Error 实例,则导航会被终止且该错误会被传递给 router.onError() 注册过的回调

router.beforeResolve:在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被调用

router.afterEach:不会接受 next 函数也不会改变导航本身

路由配置

1
2
3
4
5
6
7
8
9
10
11
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next) => {
// ...
}
}
]
})

组件内

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const Foo = {
template: `...`,
beforeRouteEnter (to, from, next) {
// 在渲染该组件的对应路由被 confirm 前调用
// 不!能!获取组件实例 `this`
// 因为守卫在导航确认前被调用,因此即将登场的新组件还没被创建。
//可以通过传一个回调给 next来访问组件实例。在导航被确认的时候执行回调,并且把组件实例作为回调方法的参数。
next(vm => { //是支持给 next 传递回调的唯一钩子,beforeRouteUpdate,beforeRouteLeave都不支持,因为可以访问的到
// 通过 `vm` 访问组件实例
})
},
beforeRouteUpdate (to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 可以访问组件实例 `this`
},
beforeRouteLeave (to, from, next) {
// 导航离开该组件的对应路由时调用
// 可以访问组件实例 `this`
}
}

完整的导航解析流程

  1. 导航被触发。
  2. 在失活的组件里调用离开守卫。
  3. 调用全局的 beforeEach 守卫。
  4. 在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。
  5. 在路由配置里调用 beforeEnter。
  6. 解析异步路由组件。
  7. 在被激活的组件里调用 beforeRouteEnter。
  8. 调用全局的 beforeResolve 守卫 (2.5+)。
  9. 导航被确认。
  10. 调用全局的 afterEach 钩子。
  11. 触发 DOM 更新。
  12. 用创建好的实例调用 beforeRouteEnter 守卫中传给 next 的回调函数。

数据获取

导航完成后获取数据

1
2
3
4
5
6
7
8
9
created () {
// 组件创建完后获取数据,
// 此时 data 已经被 observed 了
this.fetchData()
},
watch: {
// 如果路由有变化,会再次执行该方法
'$route': 'fetchData'
},

在导航完成前获取数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
beforeRouteEnter (to, from, next) {
getPost(to.params.id, (err, post) => {
next(vm => vm.setData(err, post))
})
},
// 路由改变前,组件就已经渲染完了
// 逻辑稍稍不同
beforeRouteUpdate (to, from, next) {
this.post = null
getPost(to.params.id, (err, post) => {
this.setData(err, post)
next()
})
},
methods: {
setData (err, post) {
if (err) {
this.error = err.toString()
} else {
this.post = post
}
}
}

滚动行为

scrollBehavior 这个功能只在支持 history.pushState 的浏览器中可用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const router = new VueRouter({
routes: [...],
scrollBehavior (to, from, savedPosition) {

// return 期望滚动到哪个的位置
if (to.hash) {
return {
selector: to.hash //模拟“滚动到锚点”的行为
}
}
if (savedPosition) {
return savedPosition //返回 savedPosition,在按下 后退/前进 按钮时,就会像浏览器的原生表现那样
} else {
return { x: 0, y: 0 } //让页面滚动到顶部
}
return new Promise((resolve, reject) => { //返回一个 Promise 来得出预期的位置描述
setTimeout(() => {
resolve({ x: 0, y: 0 })
}, 500)
})
}
})

My Little World

两个css问题解决

发表于 2019-11-28

子元素 position:fixed ,但宽度要和父元素一致

背景

父元素宽度随屏幕大小变化,而且margin-left有一个固定值,假设为320px
子元素position:fixed
如果直接设置子元素宽度为100%
则会导致子元素有320px的宽度在可视区域外
calc.png

知识点

css3的函数calc() 可以用来计算属性值
特点:
1.浏览器解析calc()结果还是calc(),不会计算参数表达式的值,
意味着浏览器中的值可以更加灵活,能够响应视口的改变,即实际渲染结果始终调用calc计算结果
2.可以使用加减乘除,可以套嵌使用,即calc参数表达式中可以包含calc函数调用
3.可以进行不同单位间的计算,可以混合绝对单位(如百分比与视口单元)与相对单元(比如像素)
应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
1.居中
//居中设置原来
.center {
position: absolute
top: 50%;
left: 50%;
marging-top: -150px;
margin-left: -150px;
}

//使用calc
.center {
position: absolute
top: calc(50% - 150px);
left: calc(50% - 150px);
}

2.设置字体,在页面上的任何文本,将会根据视口自动缩放,
相同比例的视口总会显示相同的文本数量,不管视口的真实尺寸是多少。
html {
font-size: calc(100vw / 30);
}

1
width:calc(100%-320px)

input样式兼容

在safari浏览器中给input设置样式不能直接被使用,会依旧使用浏览器默认样式,
此时需要给样式添加

1
2
-webkit-appearance: none;/*去除系统默认appearance的样式*/
line-height: normal;

My Little World

关于代理proxy

发表于 2019-11-08

代理是JS es6中引入,可用于控制对象
—代理可以定制对象交互时行动(例如,当读取属性或调用方法)
— 所有交互行为都必须通过代理,指定的行为发生时会调用代理方法
使用代理可以优雅实现以下内容

  • 日志记录
  • 性能测试
  • 数据校验
  • 自动填充对象属性
  • 数组负索引

使用代理对对象访问添加日志 — report 函数用于添加日志

1
2
3
4
5
6
7
8
9
10
11
12
function makeLoggable(target){
return new Proxy(target,{
get:(target,property)=>{
report('READING:'+property)
return target[property]
},
set:(target,property,value)=>{
report('writing value' +value +'to'+'property')
target[property] = value
}
})
}

proxy 用于数据校验

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
var personValidator = {
name(val){
return typeof val === 'string'
}
age(val){
return typeof val === 'number' && val>18
}
}
class person{
constructor(name,age){
this.name = name;
this.age = age;
return createValidator(this,personValidator)
}
}
function createValidator(target,validator){
return new Proxy(target,{
_validator:validator,
set(target,key,value,proxy){
if(target.hasOwnproperty(key)){
var validator = this._validator[key]
if(validator(value)){
return Reflect.set(target,key,value,proxy)
}else{
throw Error('type error')
}
}
}
})
}

使用代理自动填充属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Folder(){
return new Proxy({},{
get:(target,property)=>{
if(!(property in target)){
target[property] = new Folder()
}
return target[property]
}
})
}

const rootFolder = new Folder()
try {
rootFolder.ninjasDir.firstNinjaDir.ninjaFile = 'yoshi.txt'
}catch(e){
console.log(e)
}

使用代理实现数组负引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function creatNegativeArrayProxy(array){
if(!Array.isArray(array)){
throw new TypeError('expected an array')
}
return new Proxy(array,{
get:(target,index)=>{
index = +index
return target[index<0?target.length+index:index]
},
set:(target,index,val)=>{
index = +index
return target[index<0?target.length+index:index] = val
}
})
}

const ninja = [1,2,3]
const proxi = creatNegativeArrayProxy(ninja)
console.log(proxi[-1]) //==> 3
proxi[-2] = 22 // ninja ==>1,22,3

My Little World

absolute应用

发表于 2019-09-23

问题/背景

table 的 td 内容在内容较多时会自动进行省略处理,然后鼠标滑过弹层显示具体内容

td内容还可以配置在线编辑,鼠标滑过,可以显示小笔图标,点击小笔图标或者td块都可以进行在线编辑然后回车或者鼠标点击输入框之外的地方都可以提交发送给后台

table被包裹在一个div1中,只允许出现水平方向滚动条,不出现垂直方向滚动条

1
2
3
4
.div1{
overflow-x: auto;
overflow-y: hidden;
}

出现的问题就是,如果最后几行的内容过多,弹层显示具体内容时弹层会被遮挡,不能正常飘出来
position1.png

原理知识

父元素设置position:relative,子元素设置position:absolute,配套使用时,子元素才会会在z轴上相对父元素进行xy面的定位
如果最近父元素没有设置position:relative,那么会往父元素的父元素找,看是否设置,如果设置,则相对爷爷元素进行XY面定位,如果没有继续往上找,以此类推
即会相对父元素及以上元素中靠近自己最近的,设置了position:relative的长辈元素进行相对定位

分析

弹层没有按照预期可以弹到div1以外的地方,被困在div1里面,加上div1不能垂直方向滚动,说明弹层没有相对div1定位,
解除div1垂直方向的滚动限制,弹层把div1在垂直方向上撑起来了,滚动滚动条,可以看完整的弹层内容,说明弹层相对div1的子元素定位了,
即从盛装省略内容的标签到div1的标签中,有标签设置了position:relative,

通过查找发现,在线编辑功能的小笔图标使用了position:absolute相对定位,在它最近的div2标签上设置了position:relative,
而div2标签包含了弹层所在标签,属于div1子元素,所以弹层就会相对div2定位,沿Z轴飘在div2这一层上面,就弹不到div1以外了

解决

将div2上position:relative去掉,给在线编辑功能的小笔图标设置position:absolute的标签div3外加div4,设置position:relative

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
解决前:
<div1 style='position:relative'>
<table>
<tbody>
<tr>
<td>
<div2 style='position:relative'>
<div3 style="position:absolute"><i class="fa fa-pencil" ></i></div3>
<popwindow>具体内容</popwindow> /** 组件弹窗div设置有position:absolute**/
</div2>
</td>
</tr>
</tbody>
</table>
</div1>

解决后:
<div1 style='position:relative'>
<table>
<tbody>
<tr>
<td>
<div2>
<div4 style='position:relative'>
<div3 style="position:absolute"><i class="fa fa-pencil" ></i></div3>
</div4>
<popwindow>具体内容</popwindow> /** 组件弹窗div设置有position:absolute**/
</div2>
</td>
</tr>
</tbody>
</table>
</div1>

普通省略内容鼠标滑过
position2.png
在线编辑内容省略时,鼠标滑过
position3.png

My Little World

webpack原理

发表于 2019-07-14

bundle.js能直接运行在浏览器中的原因是:
在输出文件中通过webpack_require函数,定义了一个可以在浏览器中执行的加载函数,来模拟Nodejs中的require语句

webpack_require 函数将所有模块挂载一个对象上,通过判断该对象是否存在该模块实现模块缓存,防止二次加载

原来一个个独立的模块文件被合并到了一个单独的bundle.js的原因是
浏览器不能像Nodejs那样快速的在本地加载一个个模块文件,而必须通过网络请求去加载还未得到的文件,
如果模块的数量很多,则加载时间会很长,因此将所有模块都存放在了数组中,执行一次网络加载
输出文件分析

编写loader

常用API

获取loader配置的options

1
2
3
4
5
6
const loaderUtils = require('loader-utils')
module.exports = function(source){
//获取用户为当前loader传入的options
const options = loaderUtils.getOptions(this)
return source
}

返回除了内容之外的东西

this.callback 是 Webpack 给 Loader 注入的 API,以方便 Loader 和 Webpack 之间通信。 this.callback 的详细使用方法如下:

1
2
3
4
5
6
7
8
9
10
11
this.callback( 
// 当无法转换原内容时,给 Webpack 返回一个 Error
err: Error | null,
// 原内容转换后的内容
content: string | Buffer,
// 用于把转换后的内容得出原内容的 Source Map,方便调试
sourceMap?: SourceMap,
// 如果本次转换为原内容生成了 AST 语法树,可以把这个 AST 返回,
// 以方便之后需要 AST 的 Loader 复用该 AST,以避免重复生成 AST,提升性能
abstractSyntaxTree?: AST
);

实例:用 babel-loader 转换 ES6 代码为例,它还需要输出转换后的 ES5 代码对应的 Source Map,以方便调试源码

1
2
3
4
5
6
7
module.exports = function(source) { 
// 通过 this.callback 告诉 Webpack 返回的结果
this.callback(null, source, sourceMaps);
// 当你使用 this.callback 返回内容时,该 Loader 必须返回 undefined,
// 以让 Webpack 知道该 Loader 返回的结果在 this.callback 中,而不是 return 中
return;
};

Source Map 的生成很耗时,通常在开发环境下才会生成 Source Map,其它环境下不用生成,以加速构建。 为此 Webpack 为 Loader 提供了 this.sourceMap API 去告诉 Loader 当前构建环境下用户是否需要 Source Map。

处理异步流程

1
2
3
4
5
6
7
8
module.exports = function(source) { 
// 告诉 Webpack 本次转换是异步的,Loader 会在 callback 中回调结果
var callback = this.async();
someAsyncOperation(source, function(err, result, sourceMaps, ast) {
// 通过 callback 返回异步执行后的结果
callback(err, result, sourceMaps, ast);
});
};

处理二进制数据

1
2
3
4
5
6
7
8
9
module.exports = function(source) { 
// 在 exports.raw === true 时,Webpack 传给 Loader 的 source 是 Buffer 类型的
source instanceof Buffer === true;
// Loader 返回的类型也可以是 Buffer 类型的
// 在 exports.raw !== true 时,Loader 也可以返回 Buffer 类型的结果
return source;
};
// 通过 exports.raw 属性告诉 Webpack 该 Loader 是否需要二进制数据
module.exports.raw = true; //没有该行 Loader 只能拿到字符串。

缓存加速

在有些情况下,有些转换操作需要大量计算非常耗时,如果每次构建都重新执行重复的转换操作,构建将会变得非常缓慢。 为此,Webpack 会默认缓存所有 Loader 的处理结果,也就是说在需要被处理的文件或者其依赖的文件没有发生变化时, 是不会重新调用对应的 Loader 去执行转换操作的。
如果想让Webpack 不缓存该 Loader 的处理结果,可以这样

1
2
3
4
5
module.exports = function(source) { 
// 关闭该 Loader 的缓存功能
this.cacheable(false);
return source;
};

其他API

this.context:当前处理文件的所在目录,假如当前 Loader 处理的文件是 /src/main.js,则 this.context 就等于 /src。

this.resource:当前处理文件的完整请求路径,包括 querystring,例如 /src/main.js?name=1。

this.resourcePath:当前处理文件的路径,例如 /src/main.js。

this.resourceQuery:当前处理文件的 querystring。

this.target:等于 Webpack 配置中的 Target。

this.loadModule:当 Loader 在处理一个文件时,如果依赖其它文件的处理结果才能得出当前文件的结果时, 就可以通过 this.loadModule(request: string, callback: function(err, source, sourceMap, module)) 去获得 request 对应文件的处理结果。

this.resolve:像 require 语句一样获得指定文件的完整路径,使用方法为 resolve(context: string, request: string, callback: function(err, result: string))。

this.addDependency:给当前处理文件添加其依赖的文件,以便再其依赖的文件发生变化时,会重新调用 Loader 处理该文件。使用方法为 addDependency(file: string)。

this.addContextDependency:和 addDependency 类似,但 addContextDependency 是把整个目录加入到当前正在处理文件的依赖中。使用方法为 addContextDependency(directory: string)。

this.clearDependencies:清除当前正在处理文件的所有依赖,使用方法为 clearDependencies()。

this.emitFile:输出一个文件,使用方法为 emitFile(name: string, content: Buffer|string, sourceMap: {…})。

加载本地loader

方法一 Npm link

Npm link 专门用于开发和调试本地 Npm 模块,能做到在不发布模块的情况下,把本地的一个正在开发的模块的源码链接到项目的 node_modules 目录下,让项目可以直接使用本地的 Npm 模块。 由于是通过软链接的方式实现的,编辑了本地的 Npm 模块代码,在项目中也能使用到编辑后的代码。

完成 Npm link 的步骤如下:

1.确保正在开发的本地 Npm 模块(也就是正在开发的 Loader)的 package.json 已经正确配置好;
2.在本地 Npm 模块根目录下执行 npm link,把本地模块注册到全局;
3.在项目根目录下执行 npm link loader-name,把第2步注册到全局的本地 Npm 模块链接到项目的 node_moduels 下,其中的 loader-name 是指在第1步中的 package.json 文件中配置的模块名称。

链接好 Loader 到项目后就可以像使用一个真正的 Npm 模块一样使用本地的 Loader 了。

方法二 ResolveLoader

ResolveLoader用于配置 Webpack 如何寻找 Loader,默认情况下只会去 node_modules 目录下寻找,为了让 Webpack 加载放在本地项目中的 Loader 需要修改 resolveLoader.modules。
假如本地的 Loader 在项目目录中的 ./loaders/loader-name 中,则需要如下配置:

1
2
3
4
5
6
module.exports = { 
resolveLoader:{
// 去哪些目录下寻找 Loader,有先后顺序之分
modules: ['node_modules','./loaders/'],
}
}

编写plugin

工作原理

plugin内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class BasicPlugin{ 
// 在构造函数中获取用户给该插件传入的配置
constructor(options){
}

// Webpack 会调用 BasicPlugin 实例的 apply 方法给插件实例传入 compiler 对象
apply(compiler){
compiler.plugin('compilation',function(compilation) {
})
}
}

// 导出 Plugin
module.exports = BasicPlugin;

使用

1
2
3
4
5
6
const BasicPlugin = require('./BasicPlugin.js'); 
module.export = {
plugins:[
new BasicPlugin(options),
]
}

Webpack 启动后,在读取配置的过程中会先执行 new BasicPlugin(options) 初始化一个 BasicPlugin 获得其实例。 在初始化 compiler 对象后,再调用 basicPlugin.apply(compiler) 给插件实例传入 compiler 对象。 插件实例在获取到 compiler 对象后,就可以通过 compiler.plugin(事件名称, 回调函数) 监听到 Webpack 广播出来的事件。 并且可以通过 compiler 对象去操作 Webpack。

Compiler 和 Compilation

Compiler 和 Compilation 的含义如下:

1.Compiler 对象包含了 Webpack 环境所有的的配置信息,包含 options,loaders,plugins 这些信息,这个对象在 Webpack 启动时候被实例化,它是全局唯一的,可以简单地把它理解为 Webpack 实例;

2.Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。当 Webpack 以开发模式运行时,每当检测到一个文件变化,一次新的 Compilation 将被创建。Compilation 对象也提供了很多事件回调供插件做扩展。通过 Compilation 也能读取到 Compiler 对象。

Compiler 代表了整个 Webpack 从启动到关闭的生命周期,而 Compilation 只是代表了一次新的编译。

事件流

Webpack 就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果。 这条生产线上的每个处理流程的职责都是单一的,多个流程之间有存在依赖关系,只有完成当前处理后才能交给下一个流程去处理。 插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源做处理。
Webpack 通过 Tapable 来组织这条复杂的生产线。 Webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运作。 Webpack 的事件流机制保证了插件的有序性,使得整个系统扩展性很好。

Webpack 的事件流机制应用了观察者模式,和 Node.js 中的 EventEmitter 非常相似。 Compiler 和 Compilation 都继承自 Tapable,可以直接在 Compiler 和 Compilation 对象上广播和监听事件,方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/ 
广播出事件
event-name 为事件名称,注意不要和现有的事件重名
params 为附带的参数
/
compiler.apply('event-name',params);

/
监听名称为 event-name 的事件,当 event-name 事件发生时,函数就会被执行。
同时函数中的 params 参数为广播事件时附带的参数。
*/
compiler.plugin('event-name',function(params) {

});

compilation.apply 和 compilation.plugin 使用方法和上面一致。

在开发插件时,还需要注意以下两点:

1.只要能拿到 Compiler 或 Compilation 对象,就能广播出新的事件,所以在新开发的插件中也能广播出事件,给其它插件监听使用。
2.传给每个插件的 Compiler 和 Compilation 对象都是同一个引用。也就是说在一个插件中修改了 Compiler 或 Compilation 对象上的属性,会影响到后面的插件。
3.有些事件是异步的,这些异步的事件会附带两个参数,第二个参数为回调函数,在插件处理完任务时需要调用回调函数通知 Webpack,才会进入下一处理流程。例如:

1
2
3
4
5
6
7
compiler.plugin('emit',function(compilation, callback) { 
// 支持处理逻辑

// 处理完毕后执行 callback 以通知 Webpack
// 如果不执行 callback,运行流程将会一直卡在这不往下执行
callback();
});

常用API

监听文件变化

Webpack 会从配置的入口模块出发,依次找出所有的依赖模块,当入口模块或者其依赖的模块发生变化时, 就会触发一次新的 Compilation。
通过【watch-run】 事件可以知道是哪个文件发生变化导致了新的 Compilation

1
2
3
4
5
6
7
8
9
10
/ 当依赖的文件发生变化时会触发 watch-run 事件 
compiler.plugin('watch-run', (watching, callback) => {
// 获取发生变化的文件列表
const changedFiles = watching.compiler.watchFileSystem.watcher.mtimes;
// changedFiles 格式为键值对,键为发生变化的文件路径。
if (changedFiles[filePath] !== undefined) {
// filePath 对应的文件发生了变化
}
callback();
});

默认情况下 Webpack 只会监视入口和其依赖的模块是否发生变化,在有些情况下项目可能需要引入新的文件,例如引入一个 HTML 文件。 由于 JavaScript 文件不会去导入 HTML 文件,Webpack 就不会监听 HTML 文件的变化,编辑 HTML 文件时就不会重新触发新的 Compilation。 为了监听 HTML 文件的变化,我们需要把 HTML 文件加入到依赖列表中,为此可以使用如下代码

1
2
3
4
5
compiler.plugin('after-compile', (compilation, callback) => { 
// 把 HTML 文件添加到文件依赖列表,好让 Webpack 去监听 HTML 模块文件,在 HTML 模版文件发生变化时重新启动一次编译
compilation.fileDependencies.push(filePath);
callback();
});

读取输出资源、代码块、模块及其依赖

在 【emit】 事件发生时,代表源文件的转换和组装已经完成,在这里可以读取到最终将输出的资源、代码块、模块及其依赖,并且可以修改输出资源的内容。 插件代码如下:

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
class Plugin { 
apply(compiler) {
compiler.plugin('emit', function (compilation, callback) {
// compilation.chunks 存放所有代码块,是一个数组
compilation.chunks.forEach(function (chunk) {
// chunk 代表一个代码块
// 代码块由多个模块组成,通过 chunk.forEachModule 能读取组成代码块的每个模块
chunk.forEachModule(function (module) {
// module 代表一个模块
// module.fileDependencies 存放当前模块的所有依赖的文件路径,是一个数组
module.fileDependencies.forEach(function (filepath) {
});
});

// Webpack 会根据 Chunk 去生成输出的文件资源,每个 Chunk 都对应一个及其以上的输出文件
// 例如在 Chunk 中包含了 CSS 模块并且使用了 ExtractTextPlugin 时,
// 该 Chunk 就会生成 .js 和 .css 两个文件
chunk.files.forEach(function (filename) {
// compilation.assets 存放当前所有即将输出的资源
// 调用一个输出资源的 source() 方法能获取到输出资源的内容
let source = compilation.assets[filename].source();
});
});

// 这是一个异步事件,要记得调用 callback 通知 Webpack 本次事件监听处理结束。
// 如果忘记了调用 callback,Webpack 将一直卡在这里而不会往后执行。
callback();
})
}
}

修改输出资源

有些场景下插件需要修改、增加、删除输出的资源,要做到这点需要监听 emit 事件,因为发生 emit 事件时所有模块的转换和代码块对应的文件已经生成好, 需要输出的资源即将输出,因此 emit 事件是修改 Webpack 输出资源的最后时机。

所有需要输出的资源会存放在 compilation.assets 中,compilation.assets 是一个键值对,键为需要输出的文件名称,值为文件对应的内容。

设置 compilation.assets 的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
compiler.plugin('emit', (compilation, callback) => { 
// 设置名称为 fileName 的输出资源
compilation.assets[fileName] = {
// 返回文件内容
source: () => {
// fileContent 既可以是代表文本文件的字符串,也可以是代表二进制文件的 Buffer
return fileContent;
},
// 返回文件大小
size: () => {
return Buffer.byteLength(fileContent, 'utf8');
}
};
callback();
});

读取 compilation.assets 的代码如下:

1
2
3
4
5
6
7
8
9
compiler.plugin('emit', (compilation, callback) => { 
// 读取名称为 fileName 的输出资源
const asset = compilation.assets[fileName];
// 获取输出资源的内容
asset.source();
// 获取输出资源的文件大小
asset.size();
callback();
});

判断 Webpack 使用了哪些插件

1
2
3
4
5
6
7
8
// 判断当前配置是否使用了 ExtractTextPlugin, 
// compiler 参数即为 Webpack 在 apply(compiler) 中传入的参数
function hasExtractTextPlugin(compiler) {
// 当前配置所有使用的插件列表
const plugins = compiler.options.plugins;
// 去 plugins 中寻找有没有 ExtractTextPlugin 的实例
return plugins.find(plugin=>plugin.proto.constructor === ExtractTextPlugin) != null;
}

一个例子

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
class EndWebpackPlugin { 

constructor(doneCallback, failCallback) {
// 存下在构造函数中传入的回调函数
this.doneCallback = doneCallback;
this.failCallback = failCallback;
}

apply(compiler) {
//done:在成功构建并且输出了文件后,Webpack 即将退出时发生;
compiler.plugin('done', (stats) => {
// 在 done 事件中回调 doneCallback
this.doneCallback(stats);
});
//failed:在构建出现异常导致构建失败,Webpack 即将退出时发生;
compiler.plugin('failed', (err) => {
// 在 failed 事件中回调 failCallback
this.failCallback(err);
});
}
}
// 导出插件
module.exports = EndWebpackPlugin;

//使用
module.exports = {
plugins:[
// 在初始化 EndWebpackPlugin 时传入了两个参数,分别是在成功时的回调函数和失败时的回调函数;
new EndWebpackPlugin(() => {
// Webpack 构建成功,并且文件输出了后会执行到这里,在这里可以做发布文件操作
}, (err) => {
// Webpack 构建失败,err 是导致错误的原因
console.error(err); })
]
}

小结

流程概括

Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:

1.初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数;
(启动构建,读取与合并配置参数,加载 Plugin,实例化 Compiler。)
2.开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译;
3.确定入口:根据配置中的 entry 找出所有的入口文件;
4.编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;
5.完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系;
6.输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;
7.输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。

在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。

流程细节

1.初始化:启动构建,读取与合并配置参数,加载 Plugin,实例化 Compiler。
2.编译:从 Entry 发出,针对每个 Module 串行调用对应的 Loader 去翻译文件内容,再找到该 Module 依赖的 Module,递归地进行编译处理。
3.输出:对编译后的 Module 组合成 Chunk,把 Chunk 转换成文件,输出到文件系统。

初始化阶段事

事件名解释
初始化参数从配置文件和 Shell 语句中读取与合并参数,得出最终的参数。 这个过程中还会执行配置文件中的插件实例化语句 new Plugin()。
实例化 Compiler用上一步得到的参数初始化 Compiler 实例,Compiler 负责文件监听和启动编译。Compiler 实例中包含了完整的 Webpack 配置,全局只有一个 Compiler 实例。
加载插件依次调用插件的 apply 方法,让插件可以监听后续的所有事件节点。同时给插件传入 compiler 实例的引用,以方便插件通过 compiler 调用 Webpack 提供的 API。
environment开始应用 Node.js 风格的文件系统到 compiler 对象,以方便后续的文件寻找和读取。
entry-option读取配置的 Entrys,为每个 Entry 实例化一个对应的 EntryPlugin,为后面该 Entry 的递归解析工作做准备。
after-plugins调用完所有内置的和配置的插件的 apply 方法。
after-resolvers根据配置初始化完 resolver,resolver 负责在文件系统中寻找指定路径的文件。

编译阶段事件

事件名解释
build-module使用对应的 Loader 去转换一个模块。
normal-module-loader在用 Loader 对一个模块转换完后,使用 acorn 解析转换后的内容,输出对应的抽象语法树(AST),以方便 Webpack 后面对代码的分析。
program从配置的入口模块开始,分析其 AST,当遇到 require 等导入其它模块语句时,便将其加入到依赖的模块列表,同时对新找出的依赖模块递归分析,最终搞清所有模块的依赖关系
seal所有模块及其依赖的模块都通过 Loader 转换完成后,根据依赖关系开始生成 Chunk

输出阶段事件

事件名解释
should-emit所有需要输出的文件已经生成好,询问插件哪些文件需要输出,哪些不需要。
emit确定好要输出哪些文件后,执行文件输出,可以在这里获取和修改输出内容。
after-emit文件输出完毕。
done成功完成一次完成的编译和输出流程。
failed如果在编译和输出流程中遇到异常导致 Webpack 退出时,就会直接跳转到本步骤,插件可以在本事件中获取到具体的错误原因。

在输出阶段已经得到了各个模块经过转换后的结果和其依赖关系,并且把相关模块组合在一起形成一个个 Chunk。 在输出阶段会根据 Chunk 的类型,使用对应的模版生成最终要要输出的文件内容

My Little World

webpack优化

发表于 2019-06-29

优化开发体验

目的是提升开发效率

优化构建速度

解决项目庞大时构建的耗时加长的问题

缩小文件查找范围

1.由于Loader对文件转换很耗时,所以应该让尽可能少的文件被处理,适当调整项目目录结构,在配置loader时通过【include】属性缩小处理命中文件范围
2.当安装第三方模块都放在项目根目录的./node_module目录下时,通过指明【resolve.modules】为存放第三方模块的绝对路径,减少寻找第三方模块的递归查找
3.在项目中所有第三方模块都采用main字段去描述入口文件时,只给【resolve.mainFileds】配置main字段,不使用默认值,减少搜索步骤
4.使用【resolve.alias】进行路径映射时,将导入模块的语句,替换成直接使用模块中完整文件的语句,减少耗时的对于lib中文件的解析工作
5.优化【resolve.extension】配置,减少尝试次数
a.后缀列表尽可能小,不要将项目中不可能存在的情况写到后缀列表中
b.频率出现最高的文件后缀要优先放在最前面,以做到尽快退出寻找过程
c.在源码中写导入语句时,尽可能带上后缀,从而避免寻找过程
6.合理使用【module.noparse】属性,排除不需要进行模块解析处理的文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
1.
rules:{
//只对项目根目录下src目录中文件采用配置的loader
include:path.resolve(__dirname,'src')
}
2.
resolve:{
//使用绝对路径指明第三方模块存放的位置,减少搜索步骤
modules:[path.resolve(__dirname,'node_module')]
}
3.
resolve:{
//减少入口查找
mainFilds:['main']
}
4.
resolve:{
//减少第三方模块递归解析
alias:{
'react':path.resolve(__dirname,'./node_module/react/dist/react.min.js')
}
}

使用DLLPlugin

一个动态链接库文件可以包含为其他模块调用的函数和数据
提升构建速度的原理:
包含大量复用模块的动态链接库只需被编译一次,在之后的构建过程中被动态链接库包含的模块将不会重新编译,而是直接使用动态链接库的代码,
由于动态链接库中大多数包含的是常用的第三方模块,例如react,react-dom,所以只要不升级这些模块的版本,动态连接库就不用重新编译

DllPlugin:打包出一个个单独的动态链接库文件
DllReferencePlugin:用于在主要的配置文件中引入DllPlugin打包好的动态连接库文件

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
//webpack_dll.config.js
const path = require('path');
const DllPlugin = require('webpack/lib/DllPlugin');

module.exports = {
// JS 执行入口文件
entry: {
// 把 React 相关的放到一个单独的动态链接库
react: ['react', 'react-dom'],
// 把项目需要所有的 polyfill 放到一个单独的动态链接库
polyfill: ['core-js/fn/object/assign', 'core-js/fn/promise', 'whatwg-fetch'],
},
output: {
// 输出的动态链接库的文件名称,[name] 代表当前动态链接库的名称,也就是 entry 中配置的 react 和 polyfill
filename: '[name].dll.js',
// 输出的文件都放到 dist 目录下
path: path.resolve(__dirname, 'dist'),
// 存放动态链接库的全局变量名称,例如对应 react 来说就是 _dll_react
// 之所以在前面加上 _dll_ 是为了防止全局变量冲突
library: '_dll_[name]',
},
plugins: [
// 接入 DllPlugin
new DllPlugin({
// 动态链接库的全局变量名称,需要和 output.library 中保持一致
// 该字段的值也就是输出的 manifest.json 文件 中 name 字段的值
// 例如 react.manifest.json 中就有 "name": "_dll_react"
name: '_dll_[name]',
// 描述动态链接库的 manifest.json 文件输出时的文件名称
path: path.join(__dirname, 'dist', '[name].manifest.json'),
}),
],
};

webpack_dll.config.js文件中,DllPlugin中的name参数必须和output

//webpack.config.js
const path = require('path');
const DllReferencePlugin = require('webpack/lib/DllReferencePlugin');

module.exports = {
entry: {
// 定义 入口 Chunk
main: './main.js'
},
output: {
// 输出文件的名称
filename: '[name].js',
// 输出文件都放到 dist 目录下
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
// 项目源码使用了 ES6 和 JSX 语法,需要使用 babel-loader 转换
test: /\.js$/,
use: ['babel-loader'],
exclude: path.resolve(__dirname, 'node_modules'),
},
]
},
plugins: [
// 告诉 Webpack 使用了哪些动态链接库
new DllReferencePlugin({
// 描述 react 动态链接库的文件内容
manifest: require('./dist/react.manifest.json'),
}),
new DllReferencePlugin({
// 描述 polyfill 动态链接库的文件内容
manifest: require('./dist/polyfill.manifest.json'),
}),
],
devtool: 'source-map'
};

执行构建流程:
先编译出动态链接库相关的文件,执行webpack –config webpack_dll.config.js
在确保动态链接库存在时才能正常编译入口文件,webpack.config.js中的DllReferencePlugin会依赖动态链接库相关文件,执行webpack命令

使用HappyPack

提升构建速度原理:
将任务分解给多个子进程去并发执行,子进程处理完后再将结果发送给主进程,从而让webpack在同一时刻处理多个任务
实际表现就是
所有通过Loader处理的文件都先交给happypack/loader去处理,在收集到这些文件的处理权后,HappyPack就可以统一分配了
每通过new HappyPack() 去实例化一个HappyPack,其实就是告诉HappyPack核心调度器如何通过一系列Loader去转换一类文件,
并且可以指定如何为这类转换操作分配子进程
核心调度器的逻辑代码在主进程中,也就是运行着webpack的进程中,核心调度器会将一个个任务分配给当前空闲的子进程,
子进程处理完毕后将结果发送给核心调度,他们之间的数据交换是通过进程间的通信API实现的
核心调度器收到来自子进程处理完毕的结果后,会通知WebPack该文件已处理完毕

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
const path = require('path');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const HappyPack = require('happypack');

module.exports = {
// JS 执行入口文件
entry: {
main: './main.js',
},
output: {
// 把所有依赖的模块合并输出到一个 bundle.js 文件
filename: '[name].js',
// 输出文件都放到 dist 目录下
path: path.resolve(__dirname, './dist'),
},
module: {
rules: [
{
test: /\.js$/,
// 把对 .js 文件的处理转交给 id 为 babel 的 HappyPack 实例
use: ['happypack/loader?id=babel'],
// 排除 node_modules 目录下的文件,node_modules 目录下的文件都是采用的 ES5 语法,没必要再通过 Babel 去转换
exclude: path.resolve(__dirname, 'node_modules'),
},
{
// 把对 .css 文件的处理转交给 id 为 css 的 HappyPack 实例
test: /\.css$/,
use: ExtractTextPlugin.extract({
use: ['happypack/loader?id=css'],
}),
},
]
},
plugins: [
new HappyPack({
// 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件
id: 'babel',
// 如何处理 .js 文件,用法和 Loader 配置中一样
loaders: ['babel-loader?cacheDirectory'],
}),
new HappyPack({
id: 'css',
// 如何处理 .css 文件,用法和 Loader 配置中一样
loaders: ['css-loader'],
}),
new ExtractTextPlugin({
filename: `[name].css`,
}),
],
devtool: 'source-map' // 输出 source-map 方便直接调试 ES6 源码
};

在实例化 HappyPack 插件的时候,除了可以传入 id 和 loaders 两个参数外,HappyPack 还支持如下参数:
threads: 代表开启几个子进程去处理这一类型的文件,默认是3个,类型必须是整数。
verbose: 是否允许 HappyPack 输出日志,默认是 true。
threadPool :代表共享进程池,即多个 HappyPack 实例都使用同一个共享进程池中的子进程去处理任务,以防止资源占用过多

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
const HappyPack = require('happypack');
// 构造出共享进程池,进程池中包含5个子进程
const happyThreadPool = HappyPack.ThreadPool({ size: 5 });

module.exports = {
plugins: [
new HappyPack({
// 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件
id: 'babel',
// 如何处理 .js 文件,用法和 Loader 配置中一样
loaders: ['babel-loader?cacheDirectory'],
// 使用共享进程池中的子进程去处理任务
threadPool: happyThreadPool,
}),
new HappyPack({
id: 'css',
// 如何处理 .css 文件,用法和 Loader 配置中一样
loaders: ['css-loader'],
// 使用共享进程池中的子进程去处理任务
threadPool: happyThreadPool,
}),
new ExtractTextPlugin({
filename: `[name].css`,
}),
],
};

使用ParallelUglifyPlugin

提升构建速度原理:
当webpack有多个JS文件需要输出和压缩时,原本会使用UglifyJS去一个一个压缩再输出,但是ParallelUglifyPlugin会开启多个子进程,
将对多个文件的压缩工作分配给多个子进程完成,每个子进程其实还是通过UglifyJS去压缩代码,单变成了并行执行

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 DefinePlugin = require(‘webpack/lib/DefinePlugin’);
const ParallelUglifyPlugin = require(‘webpack-parallel-uglify-plugin’);

module.exports = {
plugins: [
// 使用 ParallelUglifyPlugin 并行压缩输出的 JS 代码
new ParallelUglifyPlugin({
// 传递给 UglifyJS 的参数
uglifyJS: {
output: {
// 最紧凑的输出
beautify: false,
// 删除所有的注释
comments: false,
},
compress: {
// 在UglifyJs删除没有用到的代码时不输出警告
warnings: false,
// 删除所有的 console 语句,可以兼容ie浏览器
drop_console: true,
// 内嵌定义了但是只用到一次的变量
collapse_vars: true,
// 提取出出现多次但是没有定义成变量去引用的静态值
reduce_vars: true,
}
},
}),
],
};

在通过 new ParallelUglifyPlugin() 实例化时,支持以下参数:
test:使用正则去匹配哪些文件需要被 ParallelUglifyPlugin 压缩,默认是 /.js$/,也就是默认压缩所有的 .js 文件。
include:使用正则去命中需要被 ParallelUglifyPlugin 压缩的文件。默认为 []。
exclude:使用正则去命中不需要被 ParallelUglifyPlugin 压缩的文件。默认为 []。
cacheDir:缓存压缩后的结果,下次遇到一样的输入时直接从缓存中获取压缩后的结果并返回。cacheDir 用于配置缓存存放的目录路径。默认不会缓存,想开启缓存请设置一个目录路径。
workerCount:开启几个子进程去并发的执行压缩。默认是当前运行电脑的 CPU 核数减去1。
sourceMap:是否输出 Source Map,这会导致压缩过程变慢。
uglifyJS:用于压缩 ES5 代码时的配置,Object 类型,直接透传给 UglifyJS 的参数。

其中的 test、include、exclude 与配置 Loader 时的思想和用法一样
UglifyES 是 UglifyJS 的变种,专门用于压缩 ES6 代码,它们两都出自于同一个项目,并且它们两不能同时使用。
UglifyES 一般用于给比较新的 JavaScript 运行环境压缩代码,例如用于 ReactNative 的代码运行在兼容性较好的 JavaScriptCore 引擎中,为了得到更好的性能和尺寸,采用 UglifyES 压缩效果会更好。
ParallelUglifyPlugin 同时内置了 UglifyJS 和 UglifyES,也就是说 ParallelUglifyPlugin 支持并行压缩 ES6 代码。
如果设置 cacheDir 开启了缓存,在之后的构建中会变的更快。

优化使用体验

通过自动化手段完成一些重复工作,让我们专注于解决问题本身

优化文件监听

通过配置watchOption进行优化
ignored:/node_module/ 不监听node_module下文件 监听更少文件
agregatetionTimeout:值越大,降低构建频率
poll:值越小,降低检查频率
后面两项的配置会使监听模式的反应和灵敏度降低

优化自动刷新性能

1.使用devserver开启inline时,devserver因为不知道某个网页依赖哪几个Chunk,所以会向每个输出的chunk中注入代理客户端代码
当项目输出多个chunk时,就会导致构建缓慢,因此关闭inline进行优化
如果不想以iframe方式去访问,但同时想让网页保持自动刷新功能,则需要手动向网页中注入代理客户端的脚本,
向index.html中注入webpack-dev-server.js,但要注意发布到线上时要删掉这段用于开发的代码

2.开启热替换时,控制台打印信息不能标明模块信息,可以使用NameModulesPlugin插件解决,从而在控制台打印出被修改的模块名称
注意关闭默认inline模式并手动注入客户端的方法,不能用于模块热替换的情况,原因在于模块热替换的运行依赖每个chunk中都包含代理客户端的代码

优化输出质量

目的是为用户呈现体验更好的网页

减少用户能够感知到的加载时间(首屏时间)

区分环境

问题:
开发过程会涉及到调试的代码,没必要发到线上给到用户
开发环境和线上接口地址不同,使用不同环境数据
线上代码会进行压缩,开发代码不需要、

解决:
当代码中使用了process模块的语句时,webpack会自动打包加入process模块代码来支持非nodejs运行环境
当代码中没有使用时,就不会打包加入
可以在源码中使用process.env.NODE_ENV环境变量去判断执行开发/线上环境的代码
环境变量的设置通过DefinePlugin设置
设置后,环境变量的值在webpack处理过程中会被代入源码中,替换掉process.env.NODE_ENV
访问proces的语句被替换,webpack也就不会在打包时加入process模块了

注意:DefinedPlugin定义的环境变量只对Webpack需要处理的代码有效,而不会影响Nodejs运行时的环境变量

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

module.exports = {
plugins: [
new DefinePlugin({
// 定义 NODE_ENV 环境变量为 production
'process.env': {
NODE_ENV: JSON.stringify('production')//环境变量的值需要一个由双引号包裹的字符串'"production"'
}
}),
],
};

压缩代码

提升网页加载速度,减少网络传输流量,混淆代码以防有人下载代码进行代码分析和改造
通过插件形式引入UglifyJs,利用UglifyJs分析JS代码语法树,理解代码含义,
从而去掉无效代码,去掉日志输出代码,缩短变量名等优化,仅用于es5
UglifyJsPlugin:封装UglifyJs实现压缩
ParallelUglifyPlugin:多进行并行处理压缩
uglifyjsWebpackPlugin:引入UglifyES,压缩es6,要去掉.babelrc文件中的babel-preset-env,否则会将es6转es5
css-loader?minimize:压缩CSS

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
1.UglifyJsPlugin
const UglifyJSPlugin = require('webpack/lib/optimize/UglifyJsPlugin');//使用内置插件
module.exports = {
plugins: [
new UglifyJSPlugin({
compress: {
// 在 UglifyES 删除没有用到的代码时不输出警告
warnings: false,
// 删除所有的 `console` 语句,可以兼容ie浏览器
drop_console: true,
// 内嵌定义了但是只用到一次的变量
collapse_vars: true,
// 提取出出现多次但是没有定义成变量去引用的静态值
reduce_vars: true,
},
output: {
// 最紧凑的输出,默认会保留空格
beautify: false,
// 删除所有的注释
comments: false,
}
}),
],
};

2.uglifyjs-webpack-plugin
const UglifyESPlugin = require('uglifyjs-webpack-plugin');
module.exports = {
plugins: [
new UglifyESPlugin({
uglifyOptions: {//相比UglifyJSPlugin 多了一层
compress: {
// 在 UglifyES 删除没有用到的代码时不输出警告
warnings: false,
// 删除所有的 `console` 语句,可以兼容ie浏览器
drop_console: true,
// 内嵌定义了但是只用到一次的变量
collapse_vars: true,
// 提取出出现多次但是没有定义成变量去引用的静态值
reduce_vars: true,
},
output: {
// 最紧凑的输出
beautify: false,
// 删除所有的注释
comments: false,
}
}
}),
],
};

3.压缩CSS
const path = require('path');
const {WebPlugin} = require('web-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
module: {
rules: [
{
test: /\.css/,// 增加对 CSS 文件的支持
// 提取出 Chunk 中的 CSS 代码到单独的文件中
use: ExtractTextPlugin.extract({
use: ['css-loader?minimize'] // 压缩 CSS 代码
}),
},
]
},
plugins: [
// 用 WebPlugin 生成对应的 HTML 文件
new WebPlugin({
template: './template.html', // HTML 模版文件所在的文件路径
filename: 'index.html' // 输出的 HTML 的文件名称
}),
new ExtractTextPlugin({
filename: `[name]_[contenthash:8].css`,// 给输出的 CSS 文件名称加上 hash 值
}),
],
};

使用CDN加速

webpack接入CDN需要满足:
1.静态资源的导入URL需要变成指向CDN服务的绝对路径URL,而不是相对于HTML文件的URL
2.静态资源的文件名需要带上由文件内容算出来的Hash值,以防止被缓存
3.将不同类型资源放到不同域名CDN服务上,以防止资源的并行加载阻塞
实现主要依赖publicPath设置存放静态资源的CDN目录URL

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
const path = require('path');
const UglifyJsPlugin = require('webpack/lib/optimize/UglifyJsPlugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const DefinePlugin = require('webpack/lib/DefinePlugin');
const {WebPlugin} = require('web-webpack-plugin');

module.exports = {
entry: {
// Chunk app 的 JS 执行入口文件
app: './main.js'
},
output: {
// 给输出的 JavaScript 文件名称加上 Hash 值
filename: '[name]_[chunkhash:8].js',
path: path.resolve(__dirname, './dist'),
// 指定存放 JavaScript 文件的线上目录
publicPath: '//js.cdn.com/id/',
},
module: {
rules: [
{
test: /\.js$/,
use: ['babel-loader'],
// 排除 node_modules 目录下的文件,node_modules 目录下的文件都是采用的 ES5 语法,没必要再通过 Babel 去转换
exclude: path.resolve(__dirname, 'node_modules'),
},
{
// 增加对 CSS 文件的支持
test: /\.css/,
// 提取出 Chunk 中的 CSS 代码到单独的文件中
use: ExtractTextPlugin.extract({
// 压缩 CSS 代码
use: ['css-loader?minimize'],
// 指定存放 CSS 中导入的资源(例如图片)的线上目录
publicPath: '//img.cdn.com/id/'
}),
},
{
// 增加对 PNG 文件的支持
test: /\.png/,
// 给输出的 PNG 文件名称加上 Hash 值
use: ['file-loader?name=[name]_[hash:8].[ext]'],
},
]
},
plugins: [
// 使用 WebPlugin 自动生成 HTML,会根据publicPath用线上地址替换原来相对地址
new WebPlugin({
// HTML 模版文件所在的文件路径
template: './template.html',
// 输出的 HTML 的文件名称
filename: 'index.html',
// 指定存放 CSS 文件的线上目录
stylePublicPath: '//css.cdn.com/id/',
}),
new ExtractTextPlugin({
// 给输出的 CSS 文件名称加上 Hash 值
filename: `[name]_[contenthash:8].css`,
}),
new DefinePlugin({
// 定义 NODE_ENV 环境变量为 production 去除开发时才需要的部分
'process.env': {
NODE_ENV: JSON.stringify('production')
}
}),
// 压缩输出的 JS 代码
],
};

使用tree Shaking剔除用不上的代码

Tree Shaking可以分析出那些代码被用上了,哪些没有
Tree Shaking正常工作的前提是,提交给webpack的js代码必须采用了ES6的模块化语法,因为es6模块化语法是静态的
(在导入,导出语句中的路径必须是静态字符串,而且不能放入其他代码块中),这让webpack可以简单地分析出那些export被import了。
实现:
1.修改.babelrc文件,保留ES6语法

1
2
3
4
5
6
7
8
9
10
{
"presets":[
[
"env",
{
"module":false//关闭模块转换功能,保留ES6语法
}
]
]
}

可以使用webpack –display-used-exports查看分析结果
2.使用uglifyJS剔除用不上的代码
可以在直接在配置文件中配置
也可以使用命令行执行
webpack –display-used-exports –optimize-minimize

处理但第三方库时,利用mainField告诉webpack采用那份入口文件,
当指定入口于文件采用es6模块化语法时,从而可以使用treeShaking进行代码优化

提取公共代码

问题:
相同资源被重复加载,浪费用户的流量和服务器成本
每个页面需要加载的资源太大,导致网页首屏加载缓慢,影响用户体验

解决:
将多个页面公共代码部分抽离成单独文件,用户在第一次访问后,公共文件代码被浏览器缓存
在用户切换其他页面时,则不会再重新加载存放公共代码的文件,而是直接从缓存中获取
从而
减少网络传输流量,降低服务器成本
虽然用户第一次打开网站的速度得不到优化,但之后访问其他页面的速度将提高

实现:
–根据网站所使用的技术栈,找出网站所有页面都需要用到的基础库(第三方库),将他们提取到一个单独的文件base.js中
该文件包含了所有网页的基础运行环境,用于长期缓存,提高响应速度

–剔除了各个页面中被base.js包含的部分代码后,再找出所有页面都依赖的公共部分的代码,将他们提取到commom.js

–再为每个网页都生成一个单独的文件,不包含base.js和common.js中包含的部分,只包含各页面单独需要的部分代码

– 依赖CommonsChunkPlugin插件
CommonsChunkPlugin实例会生成一个新的Chunk,这个chunk中包含了被提取的代码
通过name属性告诉插件新生成的chunk的名称,chunks属性指明从哪些已有chunk中提取公共部分

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
const path = require('path');
const UglifyJsPlugin = require('webpack/lib/optimize/UglifyJsPlugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const DefinePlugin = require('webpack/lib/DefinePlugin');
const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
const {AutoWebPlugin} = require('web-webpack-plugin');

// 使用 AutoWebPlugin,自动寻找 pages 目录下的所有目录,把每一个目录看成一个单页应用
const autoWebPlugin = new AutoWebPlugin('pages', {
template: './template.html', // HTML 模版文件所在的文件路径
// 提取出所有页面公共的代码
commonsChunk: {
name: 'common',// 提取出公共代码 Chunk 的名称
},
});

module.exports = {
// AutoWebPlugin 会找为寻找到的所有单页应用,生成对应的入口配置,
// autoWebPlugin.entry 方法可以获取到生成入口配置
entry: autoWebPlugin.entry({
// 这里可以加入你额外需要的 Chunk 入口
base: './base.js'
}),
output: {
filename: '[name]_[chunkhash:8].js',// 给输出的文件名称加上 hash 值
path: path.resolve(__dirname, './dist'),
},
module: {
rules: [
{
test: /\.js$/,
use: ['babel-loader'],
// 排除 node_modules 目录下的文件,node_modules 目录下的文件都是采用的 ES5 语法,没必要再通过 Babel 去转换
exclude: path.resolve(__dirname, 'node_modules'),
},
{
test: /\.css/,// 增加对 CSS 文件的支持
// 提取出 Chunk 中的 CSS 代码到单独的文件中
use: ExtractTextPlugin.extract({
use: ['css-loader?minimize'] // 压缩 CSS 代码
}),
},
]
},
plugins: [
autoWebPlugin,
// 为了从 common 中提取出 base 也包含的部分,减小common体积
new CommonsChunkPlugin({
// 从 common 和 base 两个现成的 Chunk 中提取公共的部分
chunks: ['common', 'base'],
// 把公共的部分放到 base 中
name: 'base'
}),
new ExtractTextPlugin({
filename: `[name]_[contenthash:8].css`,// 给输出的 CSS 文件名称加上 hash 值
}),
new DefinePlugin({
// 定义 NODE_ENV 环境变量为 production 去除 react 代码中的开发时才需要的部分
'process.env': {
NODE_ENV: JSON.stringify('production')
}
}),
// 压缩输出的 JS 代码
],
};

common.js没有内容的时候解决
1.CommonsChunkPlugin的minChunks属性表示文件要被提取出来时需要在指定的Chunks中出现的最小次数,值越小,被提到common中的文件越多
2.根据各个页面之间的相关性选取其中的部分页面时,可用CommonChunkPlugin提取这部分被选出的页面的公共部分,
而不是提取所有页面的公共部分,而且这样的操作可以叠加多次,缺点是配置复杂,需要根据页面之间关系去思考如何配置
但该方法不通用

分割代码按需加载

问题
单页面应用一次性加载所有功能代码,实际在每个阶段只可能使用其中一部分

解决
将整个网站划分成一个个小功能,再按照每个功能的相关程度将他们分成几类
将每一类合并成一个Chunk,按需加载对应的Chunk
不要按需加载用户首次打开网站时需要看到的画面所对应的功能,将其放到执行入口所在的chunk中,
以减少用户能感知的网页加载时间
对于不依赖大量代码的功能点,可对其进行按需加载

实现

1
2
3
4
5
6
7
//oneName.js
module.exports= function(){}

//加载语句
import(/* webpackChunkName:"oneName" */ './oneName').then((oneName)=>{
oneName()
})

webpack内置了对import(*)语句的支持,当webpack遇到类似的语句时会这样处理
以oneName.js为入口重新生成一个Chunk
当代码执行到import所在的语句时才去加载由Chunk对应生成的文件
import返回一个Promise,当文件加载成功时可以在Promise的then方法中获取oneName.js导出的内容

/* webpackChunkName:”oneName” */ 含义是为动态生成的Chunk赋予一个名称,以方便我们追踪和调试代码
如果不指定,则其默认的名称将会是[id].js
同样需要在webpack中配置chunkFilename属性,来指定动态生成的Chunk在输出时文件名称

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
const path = require('path');

module.exports = {
// JS 执行入口文件
entry: {
main: './main.js',
},
output: {
// 为从 entry 中配置生成的 Chunk 配置输出文件的名称
filename: '[name].js',
// 为动态加载的 Chunk 配置输出文件的名称
chunkFilename: '[name].js',
path: path.resolve(__dirname, './dist'),
},
module: {
rules: [
{
test: /\.js$/,
use: ['babel-loader'],
exclude: path.resolve(__dirname, 'node_modules'),
},
]
},
devtool: 'source-map' // 输出 source-map 方便直接调试 ES6 源码
};

//在.babellrc中需要引入组件来识别import(*)语法
{
"presets":[
"env",
"react"
],
"plugins":[
"syntax-dynamic-import"
]
}
//实际应用
import React, {PureComponent, createElement} from 'react';
import {render} from 'react-dom';
import {HashRouter, Route, Link} from 'react-router-dom';
import PageHome from './pages/home';

/**
* 异步加载组件
* @param load 组件加载函数,load 函数会返回一个 Promise,在文件加载完成时 resolve
* @returns {AsyncComponent} 返回一个高阶组件用于封装需要异步加载的组件
*/
function getAsyncComponent(load) {
return class AsyncComponent extends PureComponent {

componentDidMount() {
// 在高阶组件 DidMount 时才去执行网络加载步骤
load().then(({default: component}) => {
// 代码加载成功,获取到了代码导出的值,调用 setState 通知高阶组件重新渲染子组件
this.setState({
component,
})
});
}

render() {
const {component} = this.state || {};
// component 是 React.Component 类型,需要通过 React.createElement 生产一个组件实例
return component ? createElement(component) : null;
}
}
}

// 根组件
function App() {
return (
<HashRouter>
<div>
<nav>
<Link to='/'>Home</Link> | <Link to='/about'>About</Link> | <Link to='/login'>Login</Link>
</nav>
<hr/>
<Route exact path='/' component={PageHome}/>
<Route path='/about' component={getAsyncComponent(
// 异步加载函数,异步地加载 PageAbout 组件
() => import(/* webpackChunkName: 'page-about' */'./pages/about')
)}
/>
<Route path='/login' component={getAsyncComponent(
// 异步加载函数,异步地加载 PageAbout 组件
() => import(/* webpackChunkName: 'page-login' */'./pages/login')
)}
/>
</div>
</HashRouter>
)
}

// 渲染根组件
render(<App/>, window.document.getElementById('app'));

提升流畅度

提升代码性能
1.使用prepack优化代码运行时效率
通过在编译阶段预先执行源码来得到执行结果,再直接将运行结果放到编译后的代码中,而不是在代码运行时才去求值

prepack工作原理和流程大致如下:
通过Babel将JS源码解析成抽象语法树(AST),以更细粒度地分析源码
PrePack实现了一个JS解释器,用于执行源码,借助这个解释器,Prepack才能理解源码具体是如何执行的
并将执行过程中的结果返回到输出中

因为还处于开发阶段,不能识别DOM api和部分nodejs API 代码,优化后文件尺寸可能大大增加,性能可能更差所以目前还不适合用于处理线上代码

2.开启Scope Hoisting
分析模块之间的依赖关系,尽可能将被打散的模块合并到一个函数中,大前提是不能造成代码冗余,
因此只有那些被引用了一次的模块才能被合并
由于Scope Hosting需要分析模块之间的依赖关系,因此源码必须采用ES6语句,不然它将无法生效
对于非ES6模块化语法的代码,webpack会降级处理且不使用Scope Hoisting优化。
为了知道webpack对哪些代码做了降级处理,可以在启动webpack时带上–display-optimization-bailout参数
这样输出的日志就会告知是什么原因导致了降级处理

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 ModuleConcatenationPlugin = require('webpack/lib/optimize/ModuleConcatenationPlugin');

module.exports = {
// JS 执行入口文件
entry: './main.js',
output: {
// 把所有依赖的模块合并输出到一个 bundle.js 文件
filename: 'bundle.js',
// 输出文件都放到 dist 目录下
path: path.resolve(__dirname, './dist'),
},
resolve: {
// 针对 Npm 中的第三方模块优先采用 jsnext:main 中指向的 ES6 模块化语法的文件
mainFields: ['jsnext:main', 'browser', 'main']
},
plugins: [
// 开启 Scope Hoisting
new ModuleConcatenationPlugin(),
],
};

好处:
代码体积更小,因为函数申明语句会产生大量代码
代码在运行时因为创建的函数作用域变少,所以内存开销也变小了

输出分析

执行命令
webpack –profile –json >stats.json
可以将构建相关的信息输出到stats.json文件中
1.打开http:\//webpack.github.io/analyse/ 上传stats文件,使用官方Webpack Analyse分析输出结果
2.使用webpack-bundle-analyzer进行分析
安装webpack-bundle-analyzer到全局
在项目根目录中执行webpack-bundle-analyzer,浏览器会打开对应网页并展现打包结果

侧重优化输出质量的配置
侧重优化开发体验的配置

My Little World

Vue编译初探

发表于 2019-06-09

Vue声明周期
初始化之后调用$mount会挂载组件编译template

编译

compile编译可以分成parse,optimize与generate三个阶段,最终得到render function
parse
parse 会用正则等方式循环切割字符串,解析template模板中的指令,class,style等数据,形成AST
optimize
标记static静态结点,这是编译过程的一处优化
generate
将AST转化成render function字符串的过程,得到的结果是render的字符串以及staticRenderFns

当render function 被调用的时候,因为会读取所需对象的值,所以会触发getter函数进行【依赖收集】
【依赖收集】的目的是将观察者Watcher对象存放到当前闭包中的订阅者Dep的subs中
在修改对象的值的时候,会触发对应的setter,setter通知之前【依赖收集】得到的Dep中的每一个Watcher
告诉他们自己的值改变了,需要重新渲染视图

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
Vue.prototype.$mount = function(){
//挂在组件 已生成渲染函数,被调用
return mountComponent(this,el,hydrating)
}

//缓存 mount
var mount = Vue.prototype.$mount;

//重新覆盖 编译的功能
Vue,prototype.$mount = function(){
//挂在组件 生成render function
return mount.call(this,el)
}
编译过程:字符串模板转成渲染函数
运行时:调用渲染函数
独立构建 = 编译+运行时
运行时构建 = 运行时

//模板的编译 分成parse,optimize与generate三个阶段,最终得到render function
compileToFunctions(template)
//render function生成通过new Function,可以将函数功能通过字符串传递进去生成新函数
var render = new Function(参数,函数主体内容字符串)
其中函数主体字符串的功能就是返回parse生成的抽象语法树AST

渲染函数在哪被调用
mountComponen(){
//模板编译完成,实例挂载之前调用生命周期函数
callHook(vm,'beforeMount')

初始化 updateComponent函数
在非生产环境下config.performance为true,
初始化 updateComponent时有进行性能追踪的相关代码
(进行性能追踪4个场景:
组件【初始化】时
【编译】时模板转渲染函数时
通过【渲染】函数生成虚拟DOM时
【打补丁】,虚拟DOM转真实Dom时)
vm.update(vm._render(),hydrating)
vm._render() === vm.$options.render() //生成虚拟节点vnode
vm.update() 把vm._render()生成的虚拟节点渲染成真实的DOM
updateComponent 用作参数生成Watcher,即数据发生变化时,可以重新渲染DOM节点

}

vm.$option.render 渲染函数生成

1.生成
vm.$option.render = render 构造器函数调用parse,generate生成函数主体字符串,
再调用new Function返回函数
2.探究渲染函数this指向
initProxy
【渲染函数的作用域代理】
Proxy在目标对象之前架设一层拦截,拦截啥?
读取get;设置set;key in proxyObject 属性检测;with(){}
对一个new Proxy生成的对象进行上述操作时就会引发has钩子函数
vnode = render.call(vm._renderProxy,vm.$creatElement)

1
2
3
4
5
(function anonymous(){//一个渲染函数
//vm.renderProxy 访问变量A就会进行拦截
//调用proxy的has钩子函数 ,钩子函数中进行依赖收集,与生成的watcher进行绑定
with(this){return ...变量A...}
})

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
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
//vue 对象生成
(function(global,factory){
typeof exports === 'object' && typeof module !=='undefinded' ?module.export = factory():
typeof define === 'function' && define.amd ? define(factory):
(global.Vue = factory());//window.Vue cmd AMD Comonjs
})(this,function(){
var ASSET_TYPES = [
'components',
'directives',
'filters'
]
//全局配置对象 挂载在Vue对象上,用作接口,进行自定义策略
(怎样处理自定义挂载在vue上的属性,和vue自身的同名属性,二者合并options时的处理方案)
//Vue.config.optionMergeStrategies.xxxx = function(){}
var config = {
optionMergeStrategies:Object.create(null)
}
//自定义策略
var strats = config.optionMergeStrategies
strats.data = function(parentVal,childVal,vm,key){
return function mergedInstanceDataFn(){

}
}
//默认策略
function defaultStrats(parentVal,childVal,vm){
return childVal === undefined? parentVal:childVal
}
var has = function(obj,key){
return obj != null && Object.hasOwnProperty.call(obj,key)
}
function mergeOption(parent,child,vm){
var options = {}
var key
for(key in parent){ //parent=>component directive filters 本身具备的
mergeFild(key)
}
for(key in child){ //child =>el data component 外部传参进来
if(!has(parent,key)){ //拦截重复操作
mergeFild(key)
}
}
//选项的处理celve
function mergeFild(key){
//生成最终需要数据
//合并策略 自定义策略 默认策略
console.log(key)
var strat = strats[key] || defaultStrats
options[key] = strat(parent[key],child[key],vm)
}

return options
}
function initMixin(vue){

Vue.prototype._init = function(options){
var vm = this
//合并选项 VUE.option option
vm.$options = mergeOption(Vue.options,options||{},vm)
}
}
function initGlobalAPI(Vue){
var configDef = {};
configDef.get = function(){
return config
}
configDef.set = function(val){
console.error('不要修改config')
}
//Vue.config = config 对修改config做拦截,但可以对config的属性进行操作
//Vue.config.optionMergeStrategies 可以进行扩展自定义策略
Object.defineProperty(Vue,'config',configDef)
}

function Vue(options){
if(!(this instanceof Vue)){
console.error('....')
}
this._init(options)
}
Vue.options = Object.create(null);
ASSET_TYPES.foreach(function(type){
Vue.options[type] = Object.create(null)
})
initMixin(Vue)
initGlobalAPI(Vue)initGlobalAPI(Vue)
return Vue
})
1…789…25
YooHannah

YooHannah

246 日志
1 分类
21 标签
RSS
© 2025 YooHannah
由 Hexo 强力驱动
主题 - NexT.Pisces