优化环境配置
1-webpack性能优化
开发环境性能优化
生产环境性能优化
开发环境性能优化
优化打包构建速度
HMR
优化代码调试
source-map
生产环境性能优化
优化打包构建速度
oneOf
babel缓存
多进程打包
externals
dll
优化代码运行的性能
缓存(hash-chunkhash-contenthash)
tree shaking
code split
懒加载/预加载
pwa
01-HMR 热模块替换
基于开发环境的devserver(开发环境没有hmr功能)
HMR: hot module replacement 热模块替换 / 模块热替换
作用:一个模块发生变化,只会重新打包这一个模块(而不是打包所有模块) 极大提升构建速度
样式文件:可以使用HMR功能:因为style-loader内部实现了~
js文件:默认不能使用HMR功能–>需要修改js代码,添加支持HMR功能的代码。
注意:HMR功能对js的处理,只能处理非入口js文件的其他文件。
html文件: 默认不能使用HMR功能.同时会导致问题:html文件不能热更新了~ (不用做HMR功能)
解决:修改entry入口,将html文件引入
entry: ['./src/js/index.js', './src/index.html'],
...
devServer: {
static: resolve(__dirname, 'build'),
compress: true,
port: 3000,
open: true,
// 开启HMR功能
// 当修改了webpack配置,新配置要想生效,必须重新webpack服务
hot: true
}
在需要热模块替换的非入口js文件中协商如下代码,可实现该方法的热模块替换
if (module.hot) {
// 一旦 module.hot 为true,说明开启了HMR功能。 --> 让HMR功能代码生效
module.hot.accept('./print.js', function() {
// 方法会监听 print.js 文件的变化,一旦发生变化,其他模块不会重新打包构建。
// 会执行后面的回调函数
print();
});
}
复制代码
02-source-map
(开发环境) source-map: 一种 提供源代码到构建后代码映射 技术 (如果构建后代码出错了,通过映射可以追踪源代码错误)
//在webpack.config.js中加
devtool: '...'
<!--[inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map-->
<!--source-map:外部-->
<!-- 错误代码准确信息 和 源代码的错误位置-->
<!--inline-source-map:内联-->
<!-- 只生成一个内联source-map-->
<!-- 错误代码准确信息 和 源代码的错误位置-->
<!--hidden-source-map:外部-->
<!-- 错误代码错误原因,但是没有错误位置-->
<!-- 不能追踪源代码错误,只能提示到构建后代码的错误位置-->
<!--eval-source-map:内联-->
<!-- 每一个文件都生成对应的source-map,都在eval-->
<!-- 错误代码准确信息 和 源代码的错误位置-->
<!--nosources-source-map:外部-->
<!-- 错误代码准确信息, 但是没有任何源代码信息-->
<!--cheap-source-map:外部-->
<!-- 错误代码准确信息 和 源代码的错误位置 -->
<!-- 只能精确的行-->
<!--cheap-module-source-map:外部-->
<!-- 错误代码准确信息 和 源代码的错误位置 -->
<!-- module会将loader的source map加入-->
<!--内联 和 外部的区别:1. 外部生成了文件,内联没有 2. 内联构建速度更快-->
<!--开发环境:速度快,调试更友好-->
<!-- 速度快(eval>inline>cheap>...)-->
<!-- eval-cheap-souce-map-->
<!-- eval-source-map-->
<!-- 调试更友好 -->
<!-- souce-map-->
<!-- cheap-module-souce-map-->
<!-- cheap-souce-map-->
<!-- --> eval-source-map / eval-cheap-module-souce-map-->
<!--生产环境:源代码要不要隐藏? 调试要不要更友好-->
<!-- 内联会让代码体积变大,所以在生产环境不用内联-->
<!-- nosources-source-map 全部隐藏-->
<!-- hidden-source-map 只隐藏源代码,会提示构建后代码错误信息-->
<!-- --> source-map / cheap-module-souce-map-->
复制代码
03-oneof
优化生产环境打包构建速度的
以下loader只会匹配一个
注意:不能有两个配置处理同一种类型文件
oneof使一个文件只会匹配一个loader,否则会进行多次匹配。不能有两个配置处理同一种类型文件。因为eslint-loader和babel-loader都匹配js文件,所以要把一个loader放在oneof外面。
module: {
rules: [
{
// 在package.json中eslintConfig --> airbnb
test: /.js$/,
exclude: /node_modules/,
// 优先执行
enforce: 'pre',
loader: 'eslint-loader',
options: {
fix: true
}
},
{
// 以下loader只会匹配一个
// 注意:不能有两个配置处理同一种类型文件
oneOf: [
{
test: /.css$/,
use: [...commonCssLoader]
},
{
test: /.less$/,
use: [...commonCssLoader, 'less-loader']
},
/*
正常来讲,一个文件只能被一个loader处理。
当一个文件要被多个loader处理,那么一定要指定loader执行的先后顺序:
先执行eslint 在执行babel
*/
{
test: /.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
presets: [
[
'@babel/preset-env',
{
useBuiltIns: 'usage',
corejs: {version: 3},
targets: {
chrome: '60',
firefox: '50'
}
}
]
]
}
},
{
exclude: /.(js|css|less|html|jpg|png|gif)/,
loader: 'file-loader',
options: {
outputPath: 'media'
}
}
]
}
]
},
复制代码
04-缓存
类似开发环境的hmr 缓存:
babel缓存cacheDirectory: true–> 让第二次打包构建速度更快
文件资源缓存
hash: 每次wepack构建时会生成一个唯一的hash值。
问题: 因为js和css同时使用一个hash值。 如果重新打包,会导致所有缓存失效。(可能我却只改动一个文件)
chunkhash:根据chunk生成的hash值。如果打包来源于同一个chunk,那么hash值就一样
问题: js和css的hash值还是一样的 因为css是在js中被引入的,所以同属于一个chunk
contenthash:根据文件的内容生成hash值。不同文件hash值一定不一样 --> 让代码上线运行缓存更好使用。
//开启babel缓存
{
test: /.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
presets: [
[
'@babel/preset-env',
{
useBuiltIns: 'usage',
corejs: { version: 3 },
targets: {
chrome: '60',
firefox: '50'
}
}
]
],
// 开启babel缓存
// 第二次构建时,会读取之前的缓存
cacheDirectory: true
}
},
//文件资源缓存
output: {
filename: 'js/built.[contenthash:10].js',
path: resolve(__dirname, 'build')
},
复制代码
05-tree-shaking
tree shaking:去除无用代码
前提:1. 必须使用ES6模块化 2. 开启production环境
作用: 减少代码体积
在package.json中配置
"sideEffects": false 所有代码都没有副作用(都可以进行tree shaking)
问题:可能会把css / @babel/polyfill (副作用)文件干掉
"sideEffects": ["*.css", "*.less"]
复制代码
06-代码分割
入口几个文件 出口就几个文件
entry: {
// 多入口:有一个入口,最终输出就有一个bundle
index: './src/js/index.js',
test: './src/js/test.js'
},
output: {
// [name]:取文件名
filename: 'js/[name].[contenthash:10].js',
path: resolve(__dirname, 'build')
},
复制代码
可以将node_modules中代码单独打包一个chunk最终输出
/*
1. 可以将node_modules中代码单独打包一个chunk最终输出
*/
optimization: {
splitChunks: {
chunks: 'all'
}
},
复制代码
单入口
//在某一js文件中写如下配置实现输出多文件
/*
通过js代码,让某个文件被单独打包成一个chunk
import动态导入语法:能将某个文件单独打包
*/
import(/* webpackChunkName: 'test' */'./test')
.then(({ mul, count }) => {
// 文件加载成功~
// eslint-disable-next-line
console.log(mul(2, 5));
})
.catch(() => {
// eslint-disable-next-line
console.log('文件加载失败~');
});
复制代码
07-懒加载
懒加载:当文件需要使用时才加载 预加载 prefetch:会在使用之前,提前加载js文件 正常加载可以认为是并行加载(同一时间加载多个文件)
预加载 prefetch:等其他资源加载完毕,浏览器空闲了,再偷偷加载资源(预加载兼容问题有点严重)
console.log(‘index.js文件被加载了~’);
// import { mul } from './test';
document.getElementById('btn').onclick = function() {
// 懒加载~:当文件需要使用时才加载~
// 预加载 prefetch:会在使用之前,提前加载js文件
// 正常加载可以认为是并行加载(同一时间加载多个文件)
// 预加载 prefetch:等其他资源加载完毕,浏览器空闲了,再偷偷加载资源
import(/* webpackChunkName: 'test', webpackPrefetch: true */'./test').then(({ mul }) => {
console.log(mul(4, 5));
});
};
复制代码
08-PWA渐进式网络开发应用程序 (离线可访问)
workbox ---->workbox-wepack-plugin
webpack.config.js
plugins: [
new WorkboxWebpackPlugin.GenerateSW({
/*
1. 帮助serviceworker快速启动
2. 删除旧的 serviceworker
生成一个 serviceworker 配置文件~
*/
clientsClaim: true,
skipWaiting: true
})
],
复制代码
入口js文件配置
/*
1. eslint不认识 window、navigator全局变量
解决:需要修改package.json中eslintConfig配置
"env": {
"browser": true // 支持浏览器端全局变量
}
2. sw代码必须运行在服务器上
--> nodejs
-->
npm i serve -g
serve -s build 启动服务器,将build目录下所有资源作为静态资源暴露出去
*/
// 注册serviceWorker
// 处理兼容性问题
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker
.register('/service-worker.js')
.then(() => {
console.log('sw注册成功了~');
})
.catch(() => {
console.log('sw注册失败了~');
});
});
}
复制代码
09-多进程打包
下载thread-loader,对谁进行多进程打包就把thread-loader配置在它的后面
开启多进程打包。 (有利有弊) 进程启动大概为600ms,进程通信也有开销。 只有工作消耗时间比较长,才需要多进程打包
{
test: /.js$/,
exclude: /node_modules/,
use: [
/*
开启多进程打包。
进程启动大概为600ms,进程通信也有开销。
只有工作消耗时间比较长,才需要多进程打包
*/
{
loader: 'thread-loader',
options: {
workers: 2 // 进程2个
}
},
{
loader: 'babel-loader',
options: {
presets: [
[
'@babel/preset-env',
{
useBuiltIns: 'usage',
corejs: { version: 3 },
targets: {
chrome: '60',
firefox: '50'
}
}
]
],
// 开启babel缓存
// 第二次构建时,会读取之前的缓存
cacheDirectory: true
}
}
]
},
复制代码
10-externals
有些包需要用cdn引入进来,防止将这些包打包进来,使用externals忽略这个包名。
mode: 'production',
externals: {
// 拒绝jQuery被打包进来
jquery: 'jQuery'
}
复制代码
<script src="https://cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script>
复制代码
11-dll
对代码单独打包
新建webpack.dll.js
/*
使用dll技术,对某些库(第三方库:jquery、react、vue...)进行单独打包
当你运行 webpack 时,默认查找 webpack.config.js 配置文件
需求:需要运行 webpack.dll.js 文件
--> webpack --config webpack.dll.js
*/
const { resolve } = require('path');
const webpack = require('webpack');
module.exports = {
entry: {
// 最终打包生成的[name] --> jquery
// ['jquery'] --> 要打包的库是jquery
jquery: ['jquery'],
},
output: {
filename: '[name].js',
path: resolve(__dirname, 'dll'),
library: '[name]_[hash]' // 打包的库里面向外暴露出去的内容叫什么名字
},
plugins: [
// 打包生成一个 manifest.json --> 提供和jquery映射
new webpack.DllPlugin({
name: '[name]_[hash]', // 映射库的暴露的内容名称
path: resolve(__dirname, 'dll/manifest.json') // 输出文件路径
})
],
mode: 'production'
};
复制代码
webpack.config.js
const { resolve } = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin');
module.exports = {
entry: './src/index.js',
output: {
filename: 'built.js',
path: resolve(__dirname, 'build')
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html'
}),
// 告诉webpack哪些库不参与打包,同时使用时的名称也得变~
new webpack.DllReferencePlugin({
manifest: resolve(__dirname, 'dll/manifest.json')
}),
// 将某个文件打包输出去,并在html中自动引入该资源
new AddAssetHtmlWebpackPlugin({
filepath: resolve(__dirname, 'dll/jquery.js')
})
],
mode: 'production'
};
我们首先封装一个响应式处理的方法 defineReactive,通过 defineProperty 这个方法重新定义对象属性的 get 和 set 描述符,来实现对数据的劫持,每次 读取数据 的时候都会触发 get ,每次 更新数据 的时候都会触发 set ,所以我们可以在 set 中触发更新视图的方法 update 来实现一个基本的响应式处理。
/**
* @param {*} obj 目标对象
* @param {*} key 目标对象的一个属性
* @param {*} val 目标对象的一个属性的初始值
*/
function defineReactive(obj, key, val) {
// 通过该方法拦截数据
Object.defineProperty(obj, key, {
// 读取数据的时候会走这里
get() {
console.log('🚀🚀~ get:', key);
return val
},
// 更新数据的时候会走这里
set(newVal) {
// 只有当新值和旧值不同的时候 才会触发重新赋值操作
if (newVal !== val) {
console.log('🚀🚀~ set:', key);
val = newVal
// 这里是触发视图更新的地方
update()
}
}
})
}
复制代码
我们写点代码来测试一下,每 1s 修改一次 obj.foo 的值 , 并定义一个 update 方法来修改 app 节点的内容。
// html
<div id='app'>123</div>
// js
// 劫持 obj.foo 属性
const obj = {}
defineReactive(obj, 'foo', '')
// 给 obj.foo 一个初始值
obj.foo = new Date().toLocaleTimeString()
// 定时器修改 obj.foo
setInterval(() => {
obj.foo = new Date().toLocaleTimeString()
}, 1000)
// 更新视图
function update() {
app.innerHTML = obj.foo
}
复制代码
可以看到,每次修改 obj.foo 的时候,都会触发我们定义的 get 和 set ,并调用 update 方法更新了视图,到这里,一个最简单的响应式处理就完成了。
处理深层次的嵌套
一个对象通常情况下不止一个属性,所以当我们要给每个属性添加响应式的时候,就需要遍历这个对象的所有属性,给每个 key 调用 defineReactive 进行处理。
/**
* @param {*} obj 目标对象
*/
function observe(obj) {
// 先判断类型, 响应式处理的目标一定要是个对象类型
if (typeof obj !== 'object' || obj === null) {
return
}
// 遍历 obj, 对 obj 的每个属性进行响应式处理
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
// 定义对象 obj
const obj = {
foo: 'foo',
bar: 'bar',
friend: {
name: 'aa'
}
}
// 访问 obj 的属性 , foo 和 bar 都被劫持到,就不在浏览器演示了。
obj.bar = 'barrrrrrrr' // => 🚀🚀~ set: bar
obj.foo = 'fooooooooo' // => 🚀🚀~ set: foo
// 访问 obj 的属性 obj.friend.name
obj.friend.name = 'bb' // => 🚀🚀~ get: friend
复制代码
当我们访问 obj.friend.name 的时候,也只是打印出来 get: friend ,而不是 friend.name , 所以我们还要进行个 递归,把 深层次的属性 同样也做响应式处理。
function defineReactive(obj, key, val) {
// 递归
observe(val)
// 继续执行 Object.defineProperty...
Object.defineProperty(obj, key, {
... ...
})
}
// 再次访问 obj.friend.name
obj.friend.name = 'bb' // => 🚀🚀~ set: name
复制代码
递归的时机在 defineReactive 这个方法中,如果 value 是对象就进行递归,如果不是对象直接返回,继续执行下面的代码,保证 obj 中嵌套的属性都进行响应式的处理,所以当我们再次访问 obj.friend.name 的时候,就打印出了 set: name 。
处理直接赋值一个对象
上面已经实现了对深层属性的响应式处理,那么如果我直接给属性赋值一个对象呢?
const obj = {
friend: {
name: 'aa'
}
}
obj.friend = { // => 🚀🚀~ set: friend
name: 'bb'
}
obj.friend.name = 'cc' // => 🚀🚀~ get: friend
复制代码
这种赋值方式还是只打印出了 get: friend ,并没有劫持到 obj.friend.name ,那怎么办呢?我们只需要在 触发 set 的时候,判断一下 value 的类型,如果它是个对象类型,我们就对他执行 observe 方法。
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
... ...
set(newVal) {
// 只有当新值和旧值不同的时候 才会触发重新赋值操作
if (newVal !== val) {
console.log('🚀🚀~ set:', key);
// 如果 newVal 是个对象类型,再次做响应式处理。
if (typeof obj === 'object' && obj !== null) {
observe(newVal)
}
val = newVal
}
}
})
}
// 再次给 obj.friend 赋值一个对象
obj.friend = {
name: 'bb'
}
// 再次访问 obj.friend.name , 这个时候就成功的劫持到了 name 属性
obj.friend.name = 'cc' //=> 🚀~ set: name
复制代码
处理新添加一个属性
上面的例子都是操作 已经存在 的属性,那么如果我们 新添加 一个属性呢?
const obj = {}
obj.age = 18
obj.age = 20
复制代码
当我们试图修改 obj.age 的时候,什么都没有打印出来,说明并没有对 obj.age 进行响应式处理。这里也非常好理解,因为新增加的属性并没有经过 defineReactive 的处理,所以我们就需要一个方法来手动处理新添加属性这种情况。
/**
* @param {*} obj 目标对象
* @param {*} key 目标对象的一个属性
* @param {*} val 目标对象的一个属性的初始值
*/
function $set(obj, key, val) {
// vue 中在这进行了很多判断,val 是对象还是数组等等,我们就从简了
defineReactive(obj, key, val)
}
// 调用 $set 方法给 obj 添加新的属性
$set(obj, 'age', 18)
// 再次访问 obj.age
obj.age = 20 //=> 🚀🚀~ set: age
复制代码
新定义的 $set 方法,内部也是把目标属性进行了 defineReactive 处理,这时我们再次更新 obj.age 的时候,就打印出了 set: age , 也就实现了一个响应式的处理。
VUE中的数据响应式
实现简易的Vue
这是 Vue 中最基本的使用方式,创建一个 Vue 的实例,然后就可以在模板中使用 data 中定义的响应式数据了,今天我们就来完成一个简易版的 Vue 。
<div id='app'>
<p>{{counter}}</p>
<p>{{counter}}</p>
<p>{{counter}}</p>
<p my-text='counter'></p>
<p my-html='desc'></p>
<button @click='add'>点击增加</button>
<p>{{name}}</p>
<input type="text" my-model='name'>
</div>
<script>
const app = new MyVue({
el: "#app",
data: {
counter: 1,
desc: `<span style='color:red' >一尾流莺</span>`
},
methods: {
add() {
this.counter++
}
}
})
</script>
复制代码
原理
设计类型介绍
MyVue: 框架构造函数
Observer:执行数据响应化(区分数据是对象还是数组)
Compile:编译模板,初始化视图,收集依赖(更新函数,创建 watcher)
Watcher:执行更新函数(更新 dom )
Dep:管理多个 Watcher 批量更新
流程解析
初始化时通过 Observer 对数据进行响应式处理,在 Observer 的 get 的时候创建一个 Dep 的实例,用来通知更新。
初始化时通过 Compile 进行编译,解析模板语法,找到其中动态绑定的数据,从 data 中获取数据并初始化视图,把模板语法替换成数据。
同时进行一次订阅,创建一个 Watcher ,定义一个更新函数 ,将来数据发生变化时,Watcher 会调用更新函数 把 Watcher 添加到 dep 中 。
Watcher 是一对一的负责某个具体的元素,data 中的某个属性在一个视图中可能会出现多次,也就是会创建多个 Watcher,所以一个 Dep 中会管理多个 Watcher。
当 Observer 监听到数据发生变化时,Dep 通知所有的 Watcher 进行视图更新。
代码实现 - 第一回合 数据响应式
observe
observe 方法相对于上面,做了一小点的改动,不是直接遍历调用 defineReactive 了,而是创建一个 Observer 类的实例 。
// 遍历obj 对其每个属性进行响应式处理
function observe(obj) {
// 先判断类型, 响应式处理的目标一定要是个对象类型
if (typeof obj !== 'object' || obj === null) {
return
}
new Observer(obj)
}
复制代码
Observer类
Observer 类之前有解释过,它就是用来 做数据响应式 的,在它内部区分了数据是 对象 还是 数组 ,然后执行不同的响应式方案。
// 根据传入value的类型做响应的响应式处理
class Observer {
constructor(value) {
this.value = value
if (Array.isArray(value)) {
// todo 这个分支是数组的响应式处理方式 不是本文重点 暂时忽略
} else {
// 这个分支是对象的响应式处理方式
this.walk(value)
}
}
// 对象的响应式处理 跟前面讲到过的一样,再封装一层函数而已
walk(obj) {
// 遍历 obj, 对 obj 的每个属性进行响应式处理
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
}
复制代码
MVVM类(MyVue)
这一回合我们就先在实例初始化的时候,对 data 进行响应式处理,为了能用 this.key 的方式访问this.$data.key,我们需要做一层代理。
class MyVue {
constructor(options) {
// 把数据存一下
this.$options = options
this.$data = options.data
// data响应式处理
observe(this.$data)
// 代理 把 this.$data 上的属性 全部挂载到 vue实例上 可以通过 this.key 访问 this.$data.key
proxy(this)
}
}
复制代码
proxy 代理也非常容易理解,就是通过 Object.defineProperty 改变一下引用。
/**
* 代理 把 this.$data 上的属性 全部挂载到 vue实例上 可以通过 this.key 访问 this.$data.key
* @param {*} vm vue 实例
*/
function proxy(vm) {
Object.keys(vm