通过引入一个新的对象(如小图片和远程代理对象)来实现对真实对象的操作或者将新的对 象作为真实对象的一个替身,这种实现机制即为代理模式,通过引入代理对象来间接访问一 个对象,这就是代理模式的模式动机。
代理模式(Proxy Pattern) :给某一个对象提供一个代理,并由代理对象控制对原对象的引用。代理模式的英 文叫做Proxy或Surrogate,它是一种对象结构型模式。
火车票代购、房产中介、律师、海外代购、明星经纪人
代理模式包含如下角色:
时序图
一个例子-明星经纪人
abstract class Star {
abstract answerPhone(): void;
}
class Angelababy extends Star {
public available: boolean=true;
answerPhone(): void {
console.log('你好,我是Angelababy.');
}
}
class AngelababyAgent extends Star {
constructor(private angelababy: Angelababy) {
super();
}
answerPhone(): void {
console.log('你好,我是Angelababy的经纪人.');
if (this.angelababy.available) {
this.angelababy.answerPhone();
}
}
}
let angelababyAgent=new AngelababyAgent(new Angelababy());
angelababyAgent.answerPhone();
<body>
<ul id="list">
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<script>
let list=document.querySelector('#list');
list.addEventListener('click',event=>{
alert(event.target.innerHTML);
});
</script>
</body>
let express=require('express');
let path=require('path')
let app=express();
app.get('/images/loading.gif',function (req,res) {
res.sendFile(path.join(__dirname,req.path));
});
app.get('/images/:name',function (req,res) {
setTimeout(()=> {
res.sendFile(path.join(__dirname,req.path));
}, 2000);
});
app.get('/',function (req,res) {
res.sendFile(path.resolve('index.html'));
});
app.listen(8080);
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
.bg-container {
width: 600px;
height: 400px;
margin: 100px auto;
}
.bg-container #bg-image {
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<div id="background">
<button data-src="/images/bg1.jpg">背景1</button>
<button data-src="/images/bg2.jpg">背景2</button>
</div>
<div class="bg-container">
<img id="bg-image" src="/images/bg1.jpg" />
</div>
<script>
let container=document.querySelector('#background');
class BackgroundImage {
constructor() {
this.bgImage=document.querySelector('#bg-image');
}
setSrc(src) {
this.bgImage.src=src;
}
}
class LoadingBackgroundImage {
static LOADING_URL=`/images/loading.gif`;
constructor() {
this.backgroundImage=new BackgroundImage();
}
setSrc(src) {
this.backgroundImage.setSrc(LoadingBackgroundImage.LOADING_URL);
let img=new Image();
img.onload=()=> {
this.backgroundImage.setSrc(src);
}
img.src=src;
}
}
let loadingBackgroundImage=new LoadingBackgroundImage();
container.addEventListener('click', function (event) {
let src=event.target.dataset.src;
loadingBackgroundImage.setSrc(src + '?ts=' + Date.now());
});
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Lazy-Load</title>
<style>
.image {
width: 300px;
height: 200px;
background-color: #CCC;
}
.image img {
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<div class="image-container">
<div class="image">
<img data-src="/images/bg1.jpg">
</div>
<div class="image">
<img data-src="/images/bg2.jpg">
</div>
<div class="image">
<img data-src="/images/bg1.jpg">
</div>
<div class="image">
<img data-src="/images/bg2.jpg">
</div>
<div class="image">
<img data-src="/images/bg1.jpg">
</div>
<div class="image">
<img data-src="/images/bg2.jpg">
</div>
<div class="image">
<img data-src="/images/bg1.jpg">
</div>
<div class="image">
<img data-src="/images/bg2.jpg">
</div>
<div class="image">
<img data-src="/images/bg1.jpg">
</div>
<div class="image">
<img data-src="/images/bg2.jpg">
</div>
</div>
</body>
<script>
const imgs=document.getElementsByTagName('img');
const clientHeight=window.innerHeight || document.documentElement.clientHeight;
let loadedIndex=0;
function lazyload() {
for (let i=loadedIndex; i < imgs.length; i++) {
if (clientHeight - imgs[i].getBoundingClientRect().top > 0) {
imgs[i].src=imgs[i].dataset.src;
loadedIndex=i + 1;
}
}
}
lazyload();
window.addEventListener('scroll', lazyload, false);
</script>
</html>
有些时候可以用空间换时间
一个正整数的阶乘(factorial)是所有小于及等于该数的正整数的积,并且0的阶乘为1
const factorial=function f(num) {
if (num===1) {
return 1;
} else {
return (num * f(num - 1));
}
}
const proxy=function (fn) {
const cache={}; // 缓存对象
return function (num) {
if (num in cache) {
return cache[num]; // 使用缓存代理
}
return cache[num]=fn.call(this, num);
}
}
const proxyFactorial=proxy(factorial);
console.log(proxyFactorial(5));
console.log(proxyFactorial(5));
console.log(proxyFactorial(5));
指的是这样一个数列:1、1、2、3、5、8、13、21、34。在数学上,斐波那契数列以如下被以递推的方法定义:F(1)=1,F(2)=1,F(n)=F(n-1)+F(n-2)(n>=3,n∈N*)
let count=0;
function fib(n) {
count++;
return n <=2 ? 1 : fib(n - 1) + fib(n - 2);
}
var result=fib(10);
console.log(result, count);//55 110
let count=0;
const fibWithCache=(function () {
let cache={};
function fib(n) {
count++;
if (cache[n]) {
return cache[n];
}
let result=n <=2 ? 1 : fib(n - 1) + fib(n - 2);
cache[n]=result;
return result;
}
return fib;
})();
var result=fibWithCache(10);
console.log(result, count);//55 17
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
#container {
width: 200px;
height: 400px;
border: 1px solid red;
overflow: auto;
}
#container .content {
height: 4000px;
}
</style>
</head>
<body>
<div id="container">
<div class="content"></div>
</div>
<script>
function throttle(callback, interval) {
let last;
return function () {
let context=this;
let args=arguments;
let now=Date.now();
if (last) {
if (now - last >=interval) {
last=now;
callback.apply(context, args);
}
} else {
callback.apply(context, args);
last=now;
}
}
}
let lastTime=Date.now();
const throttle_scroll=throttle(()=> {
console.log('触发了滚动事件', (Date.now() - lastTime) / 1000);
}, 1000);
document.getElementById('container').addEventListener('scroll', throttle_scroll);
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
#container {
width: 200px;
height: 400px;
border: 1px solid red;
overflow: auto;
}
#container .content {
height: 4000px;
}
</style>
</head>
<body>
<div id="container">
<div class="content"></div>
</div>
<script>
function debounce(callback, delay) {
let timer;
return function () {
let context=this;
let args=arguments;
if (timer)
clearTimeout(timer);
timer=setTimeout(()=> {
callback.apply(context, args);
}, delay);
}
}
let lastTime=Date.now();
const throttle_scroll=debounce(()=> {
console.log('触发了滚动事件', (Date.now() - lastTime) / 1000);
}, 1000);
document.getElementById('container').addEventListener('scroll', throttle_scroll);
</script>
</body>
</html>
<body>
<ul id="todos">
</ul>
<script>
let todos=document.querySelector('#todos');
window.onload=function(){
fetch('/todos').then(res=>res.json()).then(response=>{
todos.innerHTML=response.map(item=>`<li "><input value="${item.id}" type="checkbox" ${item.completed?"checked":""}/>${item.text}</li>`).join('');
});
}
function toggle(id){
fetch(`/toggle/${id}`).then(res=>res.json()).then(response=>{
console.log('response',response);
});
}
todos.addEventListener('click',function(event){
let checkbox=event.target;
let id=checkbox.value;
toggle(id);
});
</script>
</body>
app.js
let express=require('express');
let app=express();
app.use(express.static(__dirname));
let todos=[
{id: 1,text: 'a',completed: false},
{id: 2,text: 'b',completed: false},
{id: 3,text: 'c',completed: false},
];
app.get('/todos',function (req,res) {
res.json(todos);
});
app.get('/toggle/:id',function (req,res) {
let id=req.params.id;
todos=todos.map(item=> {
if (item.id==id) {
item.completed=!item.completed;
}
return item;
});
res.json({code:0});
});
app.listen(8080);
todos.html
<body>
<ul id="todos">
</ul>
<script>
let todos=document.querySelector('#todos');
window.onload=function(){
fetch('/todos').then(res=>res.json()).then(response=>{
todos.innerHTML=response.map(item=>`<li "><input value="${item.id}" type="checkbox" ${item.completed?"checked":""}/>${item.text}</li>`).join('');
});
}
function toggle(id){
fetch(`/toggle/${id}`).then(res=>res.json()).then(response=>{
console.log('response',response);
});
}
let LazyToggle=(function(id){
let ids=[];
let timer;
return function(id){
ids.push(id);
if(timer){
clearTimeout(timer);
}
timer=setTimeout(function(){
toggle(ids.join(','));
ids=null;
clearTimeout(timer);
timer=null;
},2000);
}
})();
todos.addEventListener('click',function(event){
let checkbox=event.target;
let id=checkbox.value;
LazyToggle(id);
});
</script>
app.js
app.get('/toggle/:ids',function (req,res) {
let ids=req.params.ids;
ids=ids.split(',').map(item=>parseInt(item));
todos=todos.map(item=> {
if (ids.includes(item.id)) {
item.completed=!item.completed;
}
return item;
});
res.json({code:0});
});
proxy-server.js
const http=require('http');
const httpProxy=require('http-proxy');
//创建一个代理服务
const proxy=httpProxy.createProxyServer();
//创建http服务器并监听8888端口
let server=http.createServer(function (req, res) {
//将用户的请求转发到本地9999端口上
proxy.web(req, res, {
target: 'http://127.0.0.1:9999'
});
//监听代理服务错误
proxy.on('error', function (err) {
console.log(err);
});
});
server.listen(8888, '0.0.0.0');
real-server.js
const http=require('http');
let server=http.createServer(function (req, res) {
res.end('9999');
});
server.listen(9999, '0.0.0.0');
otherWindow.postMessage(message, targetOrigin, [transfer]);
window.addEventListener("message", receiveMessage, false);
origin.js
let express=require('express');
let app=express();
app.use(express.static(__dirname));
app.listen(3000);
target.js
let express=require('express');
let app=express();
let bodyParser=require('body-parser');
app.use(bodyParser.urlencoded({ extended: true }));
app.use(express.static(__dirname));
let users=[];
app.post('/register', function (req, res) {
let body=req.body;
let target=body.target;
let callback=body.callback;
let username=body.username;
let password=body.password;
let user={ username, password };
let id=users.length==0 ? 1 : users[users.length - 1].id + 1;
user.id=id;
users.push(user);
res.status(302);
res.header('Location', `${target}?callback=${callback}&args=${id}`);
res.end();
});
app.listen(4000);
reg.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<script type="text/javascript">
window.addEventListener('message', function (event) {
console.log(event.data);
if (event.data.receiveId) {
alert('用户ID=' + event.data.receiveId);
}
})
</script>
<iframe name="proxyIframe" id="proxyIframe" frameborder="0"></iframe>
<form action="http://localhost:4000/register" method="POST" target="proxyIframe">
<input type="hidden" name="callback" value="receiveId">
<input type="hidden" name="target" value="http://localhost:3000/target.html">
用户名<input type="text" name="username" />
密码<input type="text" name="password" />
<input type="submit" value="提交">
</form>
</body>
</html>
target.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<script>
window.onload=function () {
var query=location.search.substr(1).split('&');
let callback, args;
for (let i=0, len=query.length; i < len; i++) {
let item=query[i].split('=');
if (item[0]=='callback') {
callback=item[1];
} else if (item[0]=='args') {
args=item[1];
}
}
try {
window.parent.postMessage({ [callback]: args }, '*');
} catch (error) {
console.log(error);
}
}
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>jquery proxy</title>
</head>
<body>
<button id="btn">点我变红</button>
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<script>
let btn=document.getElementById('btn');
btn.addEventListener('click', function () {
setTimeout($.proxy((function () {
$(this).css('color', 'red');
}), this), 1000);
});
</script>
</body>
</html>
function proxy(fn, context) {
return function () {
return fn.call(context, arguments);
}
}
let wang={
name: 'wanglaoshi',
age: 29,
height:165
}
let wangMama=new Proxy(wang,{
get(target,key) {
if (key=='age') {
return wang.age-1;
} else if (key=='height') {
return wang.height-5;
}
return target[key];
},
set(target,key,val) {
if (key=='boyfriend') {
let boyfriend=val;
if (boyfriend.age>40) {
throw new Error('太老');
} else if (boyfriend.salary<20000) {
throw new Error('太穷');
} else {
target[key]=val;
return true;
}
}
}
});
console.log(wangMama.age);
console.log(wangMama.height);
wangMama.boyfriend={
age: 41,
salary:3000
}
Vue2 中的变化侦测实现对 Object 及 Array 分别进行了不同的处理,Object 使用了 Object.defineProperty API,Array使用了拦截器对 Array 原型上的能够改变数据的方法进行拦截。虽然也实现了数据的变化侦测,但存在很多局限 ,比如对象新增属性无法被侦测,以及通过数组下边修改数组内容,也因此在 Vue2 中经常会使用到 $set 这个方法对数据修改,以保证依赖更新。
Vue3 中使用了 es6 的 Proxy API对数据代理,没有像 Vue2 中对原数据进行修改,只是加了代理包装,因此首先性能上会有所改善。其次解决了 Vue2 中变化侦测的局限性,可以不使用 $set 新增的对象属性及通过下标修改数组都能被侦测到。
适配器提供不同接口,代理模式提供一模一样的接口
装饰器模式原来的功能不变还可以使用,代理模式改变原来的功能
是解决方案实际上更像是webpack的插件索引。
写这一篇的目的是为了形成一个所以,将来要用时直接来查找即可。
解决方案:使用插件 html-webpack-plugin
webpack.config.js如下:
module.exports={
entry: './src/app.js',
output: {
path: __dirname + '/dist',
filename: 'app.bundle.js'
},
plugins: [new HtmlWebpackPlugin({
template: './src/模板文件.html',
filename: '构建的.html',
minify: {
collapseWhitespace: true,
},
hash: true,
})]
};
注意要有path,因为这个输出的html需要知道输出目录
loader用于对模块的源代码进行预处理转换。
解决方案:使用css-loader,style-loader
看一下项目结构:
此时运行webpack命令会抛出错误:
接下来安装 css-loader 和 style-loader
npm install --save-dev css-loader style-loader
再修改webpack.config.js为:
这其中rules数组就是loader用来的匹配和转换资源的规则数组。
test代表匹配需转换文件的正则表达式,而图中表示匹配所有以css结尾的文件。
而use数组代表用哪些loader去处理这些匹配到的文件。
此时再运行webpack,打包后的文件bundle.js就包含了css代码。
其中css-loader负责加载css,打包css到js中。
而style-loader负责生成:在js运行时,将css代码通过style标签注入到dom中。
解决方案:使用less-loader
但是用less-loader只是将LESS代码转换为css代码。如果要打包文件到js中,还是需要用到上面提到的css-loader和style-loader。
看一下项目结构:
然后app.js的代码为:
import styles from './app.less';
console.info('我是一个js文件123')
为了解决这种情况,首先要安装 less-loader,而less-loader是基于less的,所以也要安装less。
npm i --save-dev less less-loader
修改webpack.config.js为:
module: {
rules: [
{
test: /\.less$/,
use: [ 'style-loader', 'css-loader', 'less-loader' ]
}
]
}
很多时候我们想要的效果并不是想要把几个LESS或者CSS处理好后,打包到一个js中,而是想要把它打包到一个css文件中。
此时就有了插件 extract-text-webpack-plugin。
首先进行安装
npm i --save-dev extract-text-webpack-plugin
然后修改webpack.config.js为:
红框中为新加或修改的配置
与原配置对比可以发现,比html-webpack-plugin这个插件多做了一步,就是在匹配和转换规则里面的use中使用了ExtractTextPlugin.extract。
注意这里的fallback表示,在提取文件失败后,将继续使用style-loader去打包到js中。
此时运行webpack
可以发现输出目录build下生成了一个style.css文件,也就是我们在webpack.config.js中期望生成的文件,并且在生成的demo.html中被引用了。
webpack-dev-server可以在本地搭建一个简单的开发环境用的服务器,自动打开浏览器,而且还可以达到webpack -watch的效果。
首先安装一下:
npm i -g webpack-dev-server
npm i --save-dev webpack-dev-server
这里不需要改动webpack.config.js,直接运行命令
webpack-dev-server
查看控制台输出:
控制台输出
显示项目运行在http://localhost:8080/
webpack的输出目录的路径在/下面
并且这个服务器会自动识别输出目录下名为index的HTML文件,而我们之前输出的文件名为demo.html。
所以还需要将之前html-webpack-plugin中配置的filename改为index.html,或者直接用http://localhost:8080/demo.html也行。
当我们修改了源代码后,打开的网页还会自动更新。
为了更灵活的应用开发环境的服务器,也可以在webpack.config.js中加入如下代码:
devServer配置功能
port修改端口为8787,而不是默认的8080。
open为true表示会自动打开浏览器,而不是需要我们再手动打开浏览器并在里面输入http://localhost:8080。
compress对本地server返回的文件提供gzip压缩
index指定网站首页映射的文件,默认为index.html
这里说是ES6,实际上可以认为是ECMAScript的高版本代码,只是代指而已。
babel的作用是将浏览器还未支持的这些高版本js代码转换成可以被指定浏览器支持的js代码。
这里列出可以转换的大致语法:
babel-preset-env支持的转换
那么首先就需要安装babel
npm install babel-core babel-preset-env --save-dev
然后,为了和webpack结合起来,要用到babel-loader
npm install babel-loader --save-dev
然后在webpack.config.js的rules数组中增加以下代码:
{
test: /\.js$/,
exclude: /(node_modules)/,
use: {
loader: 'babel-loader',
options: {
presets: ['env']
}
}
}
这行代码的意思是用babel-loader解析除了node_modules文件下的所有js文件。
而babel-loader就是用babel去解析js代码。
options的内容类似于.babelrc文件的配置,有了这个就不需要.babelrc文件了。
presets表示预处理器,现在的babel不像以前需要很多预处理器了,只需要env这一个就够了。
修改之前的app.js中的代码为:
console.info('我是一个js文件123')
const doSomething=()=> {
console.info('do do do')
}
使用webpack命令后,可以看到我们最后的打包js文件中代码变成了这样:
以下为这些新增函数:
安装:
npm install --save-dev babel-polyfill
为了确保babel-polyfill被最先加载和解析,所以一般都是讲babel-polyfill在最开始的脚本中引入。
而在webpack中,就是在放到entry中,所以需要修改webpack.config.js中的配置为:
安装
npm install --save-dev babel-preset-react
配置:
这里是匹配所有以js或者jsx结尾的文件,并用 babel-preset-env和babel-preset-react进行解析
这里首先介绍一下nodejs的path模块的一个功能:resolve。
将相对路径转换为绝对路径。
在最开始引用path模块
var path=require('path');
然后可以在输出设置那里修改代码为:
output: {
path: path.resolve(__dirname, 'build'),
filename: 'bundle.js'
},
和我们原来的代码没有任何区别。
hash和chunkhash有区别,hash的话输出的文件用的都是同一个hash值,而chunkhash的话是根据模块来计算的,每个输出文件的hash值都不一样。
直接将输出文件改为
output: {
path: path.resolve(__dirname, 'build'),
filename: 'bundle.[chunkhash].js'
},
[chunkhash]就代表一串随机的hash值
当我们像上面一样不断改变输出文件时,之前的输出文件并没有去掉。
为了解决这个问题就需要clean-webpack-plugin。
首先安装
npm i clean-webpack-plugin --save-dev
然后引用插件,并声明每次生成输出需要清空的文件夹
var CleanWebpackPlugin=require('clean-webpack-plugin');
var pathsToClean=[
'build',
]
再在插件配置中加入:
new CleanWebpackPlugin(pathsToClean)
之前的webpack-dev-server提供了监听功能,只要代码改变,浏览器就会刷新。
但是模块热替换是不会刷新浏览器,只刷新修改到的那部分模块。
模块热替换无需安装。
首先需要引入模块
var webpack=require('webpack')
其实插件中加入:
new webpack.NamedModulesPlugin(),
new webpack.HotModuleReplacementPlugin()
此时运行webpack可能会报错,我们需要把之前在输出环境中写的[chunkhash]改为[hash]
可以在脚本中这么写:
"scripts": {
"dev": "webpack-dev-server",
"prod": "set NODE_ENV=production && webpack -p"
},
这样在webpack.config.js中这样修改上面的东西:
if (isProduction) {
config.output.filename='bundle.[chunkhash].js'
} else {
config.plugins.push(new webpack.NamedModulesPlugin())
config.plugins.push(new webpack.HotModuleReplacementPlugin())
}
这样就可以根据环境的不同来运行不同的配置
上述设置环境变量的脚本中只有在window下才有效,在linux和mac上需要使用
"prod": "NODE_ENV=production webpack -p"
为了解决这个问题,使得不同平台的人能公用一套代码,我们可以使用cross-env。
首先进行安装:
npm i --save-dev cross-env
然后命令直接使用类似于mac上的用法即可
"prod": "cross-env NODE_ENV=production webpack -p"
file-loader可以用来处理图片和字体文件在css文件中的路径问题,输出的css文件中会引用输出的文件地址。
html-loader可以用来处理html中,比如img元素的图片路径问题。
首先安装
npm i --save-dev file-loader html-loader
配置:
{
test: /\.(gif|png|jpe?g|svg)$/i,
use: {
loader: 'file-loader',
options: {
name: '[name].[ext]',
outputPath: 'src/images/'
}
}
},
{
test: /\.html$/,
use: [{
loader: 'html-loader',
options: {
minimize: true
}
}],
}
安装:
npm i --save-dev image-webpack-loader
配置:
{
test: /\.(gif|png|jpe?g|svg)$/i,
use: [{
loader: 'file-loader',
options: {
name: '[name].[ext]',
outputPath: 'images/'
}
},
{
loader: 'image-webpack-loader',
options: {
bypassOnDebug: true,
}
}
]
},
这里的options中也可以具体配置各个图片类型的压缩质量
如果我们用web-dev-server运行我们的输出文件,发现其中有些BUG,然后打开开发者工具取定位文件的时候,只会定位到我们的输出文件。
而这些输出文件是经过处理的,我们只有找到我们的源文件代码,然后进行相应的修改才能解决问题。
于是这里我们需要用到source-map。
很简单,在webpack.config.js中加入如下配置即可:
devtool: 'source-map',
就这么简单,还不需要安装什么插件。
但是这只对js有效,如果我们的css出现错误了呢,答案就是如下配置:
在这些loader后面加上?sourceMap即可
之前我们通过在命令中设置环境变量,并且通过环境变量来判断环境来进行不同的配置。
现在我们用官方推荐的方法来分离生产环境和开发环境的配置文件。
我们将webpack.config.js分为三个文件
webpack.common.js
webpack.dev.js
webpack.prod.js
其中webpack.common.config.js为生产环境和开发环境共有的配置,dev为开发环境独有的配置,prod为生成环境独有的配置。
而想要合成真正的配置文件,还需要一个工具:webpack-merge。
npm install --save-dev webpack-merge
以下是我们之前的webpack.config.js代码:
var ExtractTextPlugin=require('extract-text-webpack-plugin')
var HtmlWebpackPlugin=require('html-webpack-plugin')
var CleanWebpackPlugin=require('clean-webpack-plugin')
var path=require('path')
var webpack=require('webpack')
var pathsToClean=[
'build',
]
var isProduction=process.env.NODE_ENV==='production'
var config={
entry: ['babel-polyfill', './src/app.js'],
output: {
path: path.resolve(__dirname, 'build'),
filename: '[name].[hash].js'
},
devtool: 'source-map',
devServer: {
port: 8787,
open: true,
compress: true,
index: 'demo.html'
},
plugins: [
new HtmlWebpackPlugin({
template: './template/index.html',
filename: 'demo.html',
minify: {
collapseWhitespace: true,
},
hash: true
}),
new ExtractTextPlugin({ filename: 'style.css', allChunks: false }),
new CleanWebpackPlugin(pathsToClean)
],
module: {
rules: [{
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: ['css-loader?sourceMap']
})
},
{
test: /\.less$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: ['css-loader?sourceMap', 'less-loader?sourceMap']
})
},
{
test: /\.jsx?$/,
exclude: /(node_modules)/,
use: {
loader: 'babel-loader',
options: {
presets: ['env', 'react']
}
}
},
{
test: /\.(gif|png|jpe?g|svg)$/i,
use: [{
loader: 'file-loader',
options: {
name: '[name].[ext]',
outputPath: 'images/'
}
},
{
loader: 'image-webpack-loader',
options: {
bypassOnDebug: true,
}
}
]
},
{
test: /\.html$/,
use: [{
loader: 'html-loader',
options: {
minimize: true
}
}],
}
]
}
};
if (isProduction) {
config.output.filename='[name].[chunkhash].js'
} else {
config.plugins.push(new webpack.NamedModulesPlugin())
config.plugins.push(new webpack.HotModuleReplacementPlugin())
}
module.exports=config
接下来分为三个文件,webpack.common.js:
var ExtractTextPlugin=require('extract-text-webpack-plugin')
var HtmlWebpackPlugin=require('html-webpack-plugin')
var CleanWebpackPlugin=require('clean-webpack-plugin')
var path=require('path')
var webpack=require('webpack')
var pathsToClean=[
'build',
]
var isProduction=process.env.NODE_ENV==='production'
module.exports={
entry: ['babel-polyfill', './src/app.js'],
output: {
path: path.resolve(__dirname, 'build'),
filename: '[name].[chunkhash].js'
},
plugins: [
new HtmlWebpackPlugin({
template: './template/index.html',
filename: 'demo.html',
minify: {
collapseWhitespace: true,
},
hash: isProduction
}),
new ExtractTextPlugin({ filename: '[name].[contenthash].css', allChunks: false }),
new CleanWebpackPlugin(pathsToClean)
],
module: {
rules: [{
test: /\.jsx?$/,
exclude: /(node_modules)/,
use: {
loader: 'babel-loader',
options: {
presets: ['env', 'react']
}
}
},
{
test: /\.(gif|png|jpe?g|svg)$/i,
use: [{
loader: 'file-loader',
options: {
name: '[name].[ext]',
outputPath: 'images/'
}
},
{
loader: 'image-webpack-loader',
options: {
bypassOnDebug: true,
}
}
]
},
{
test: /\.html$/,
use: [{
loader: 'html-loader',
options: {
minimize: true
}
}],
}
]
}
};
然后是webpack.dev.js:
const merge=require('webpack-merge');
const common=require('./webpack.common.js');
const webpack=require('webpack');
const ExtractTextPlugin=require('extract-text-webpack-plugin')
module.exports=merge(common, {
output: {
filename: '[name].[hash].js'
},
devtool: 'source-map',
devServer: {
port: 8787,
open: true,
compress: true,
index: 'demo.html'
},
plugins: [
new webpack.NamedModulesPlugin(),
new webpack.HotModuleReplacementPlugin()
],
module: {
rules: [{
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: ['css-loader?sourceMap']
})
},
{
test: /\.less$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: ['css-loader?sourceMap', 'less-loader?sourceMap']
})
}
]
}
});
最后是webpack.prod.js:
const merge=require('webpack-merge');
const common=require('./webpack.common.js');
const ExtractTextPlugin=require('extract-text-webpack-plugin')
module.exports=merge(common, {
module: {
rules: [{
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: ['css-loader']
})
},
{
test: /\.less$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: ['css-loader', 'less-loader']
})
}
]
}
});
然后修改一下package.json中的脚本即可。
"scripts": {
"dev": "webpack-dev-server --config webpack.dev.js",
"prod": "cross-env NODE_ENV=production webpack -p --config webpack.prod.js"
},
各个插件以及loader的玩法还有很多,这里不具体介绍。
对于MVVM的理解
MVVM是Model-View-ViewModel缩写,也就是把MVC中的Controller演变成ViewModel。Model层代表数据模型,View代表UI组件,ViewModel是View和Model层的桥梁,数据会绑定到viewModel层并自动将数据渲染到页面中,视图变化的时候会通知viewModel层更新数据。
答:总共分为8个阶段创建前/后,载入前/后,更新前/后,销毁前/后
生命周期是什么
Vue 实例有一个完整的生命周期,也就是从开始创建、初始化数据、编译模版、挂载Dom -> 渲染、更新 -> 渲染、卸载等一系列过程,我们称这是Vue的生命周期
各个生命周期的作用
生命周期描述beforeCreate组件实例被创建之初,组件的属性生效之前created组件实例已经完全创建,属性也绑定,但真实dom还没有生成,$el还不可用beforeMount在挂载开始之前被调用:相关的 render 函数首次被调用mountedel 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子beforeUpdate组件数据更新之前调用,发生在虚拟 DOM 打补丁之前update组件数据更新之后activitedkeep-alive专属,组件被激活时调用deadctivatedkeep-alive专属,组件被销毁时调用beforeDestory组件销毁前调用destoryed组件销毁后调用
image
由于Vue会在初始化实例时对属性执行getter/setter转化,所以属性必须在data对象上存在才能让Vue将它转换为响应式的。Vue提供了$set方法用来触发视图更新
export default {
data(){
return {
obj: {
name: 'fei'
}
}
},
mounted(){
this.$set(this.obj, 'sex', 'man')
}
}
什么是vue生命周期?
vue生命周期的作用是什么?
vue生命周期总共有几个阶段?
第一次页面加载会触发哪几个钩子?
DOM 渲染在哪个周期中就已经完成?
父组件与子组件传值
父组件传给子组件:子组件通过props方法接受数据;
非父子组件间的数据传递,兄弟组件传值
eventBus,就是创建一个事件中心,相当于中转站,可以用它来传递事件和接收事件。项目比较小时,用这个比较合适(虽然也有不少人推荐直接用VUEX,具体来说看需求)
首页可以控制导航跳转,beforeEach,afterEach等,一般用于页面title的修改。一些需要登录才能调整页面的重定向功能。
image
image
modules:项目特别复杂的时候,可以让每一个模块拥有自己的state、mutation、action、getters,使得结构非常清晰,方便管理
image
将当前组件的<style>修改为<style scoped>
keep-alive可以实现组件缓存,当组件切换时不会对当前组件进行卸载
比如有一个列表和一个详情,那么用户就会经常执行打开详情=>返回列表=>打开详情…这样的话列表和详情都是一个频率很高的页面,那么就可以对列表组件使用<keep-alive></keep-alive>进行缓存,这样用户每次返回列表的时候,都能从缓存中快速渲染,而不是重新渲染
提供一个在页面上已存在的 DOM元素作为 Vue实例的挂载目标.可以是 CSS 选择器,也可以是一个 HTMLElement 实例,
问题一:构建的 vue-cli 工程都到了哪些技术,它们的作用分别是什么?
问题二:vue-cli 工程常用的 npm 命令有哪些?
npm install
npm run dev
npm run build
npm run build --report
在浏览器上自动弹出一个 展示 vue-cli 工程打包后 app.js、manifest.js、vendor.js 文件里面所包含代码的页面。可以具此优化 vue-cli 生产环境部署的静态资源,提升 页面 的加载速度
nextTick可以让我们在下次 DOM 更新循环结束之后执行延迟回调,用于获得更新后的 DOM
声明式(标签跳转)
<router-link :to="index">
编程式( js跳转)
router.push('index')
其基本实现原理
Vue SSR 的实现,主要就是把 Vue 的组件输出成一个完整 HTML, vue-server-renderer 就是干这事的
实现时,主要如下
可以简单理解成以下步骤:
用 timeline 工具。大意是通过 timeline 来查看每个函数的调用时常,定位出哪个函数的问题,从而能判断哪个组件出了问题
使用了v-if的时候,如果值为false,那么页面将不会有这个html标签生成。v-show则是不管值为true还是false,html元素都会存在,只是CSS中的display显示或隐藏
语法:v-bind:title="msg"简写::title="msg"
Object.defineProperty() 的问题主要有三个:
Proxy 在 ES2015 规范中被正式加入,它有以下几个特点
除了上述两点之外,Proxy 还拥有以下优势:
全局守卫
vue-router全局有三个守卫
// main.js 入口文件
import router from './router'; // 引入路由
router.beforeEach((to, from, next)=> {
next();
});
router.beforeResolve((to, from, next)=> {
next();
});
router.afterEach((to, from)=> {
console.log('afterEach 全局后置钩子');
});
路由独享守卫
如果你不想全局配置守卫的话,你可以为某些路由单独配置守卫
const router=new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next)=> {
// 参数用法什么的都一样,调用顺序在全局前置守卫后面,所以不会被全局守卫覆盖
// ...
}
}
]
})
路由组件内的守卫
组件之间通讯分为三种: 父传子、子传父、兄弟组件之间的通讯
1. 父组件给子组件传值
<template>
<child :msg="message"></child>
</template>
<script>
import child from './child.vue';
export default {
components: {
child
},
data () {
return {
message: 'father message';
}
}
}
</script>
子组件vue模板child.vue:
<template>
<div>{{msg}}</div>
</template>
<script>
export default {
props: {
msg: {
type: String,
required: true
}
}
}
</script>
2. 子组件向父组件通信
父组件向子组件传递事件方法,子组件通过$emit触发事件,回调给父组件
父组件vue模板father.vue:
<template>
<child @msgFunc="func"></child>
</template>
<script>
import child from './child.vue';
export default {
components: {
child
},
methods: {
func (msg) {
console.log(msg);
}
}
}
</script>
子组件vue模板child.vue:
<template>
<button @click="handleClick">点我</button>
</template>
<script>
export default {
props: {
msg: {
type: String,
required: true
}
},
methods () {
handleClick () {
//........
this.$emit('msgFunc');
}
}
}
</script>
3. 非父子, 兄弟组件之间通信
vue2中废弃了broadcast广播和分发事件的方法。父子组件中可以用props和$emit()。如何实现非父子组件间的通信,可以通过实例一个vue实例Bus作为媒介,要相互通信的兄弟组件之中,都引入Bus,然后通过分别调用Bus事件触发和监听来实现通信和参数传递。Bus.js可以是这样:
import Vue from 'vue'
export default new Vue()
在需要通信的组件都引入Bus.js:
<template>
<button @click="toBus">子组件传给兄弟组件</button>
</template>
<script>
import Bus from '../common/js/bus.js'
export default{
methods: {
toBus () {
Bus.$emit('on', '来自兄弟组件')
}
}
}
</script>
另一个组件也import Bus.js 在钩子函数中监听on事件
import Bus from '../common/js/bus.js'
export default {
data() {
return {
message: ''
}
},
mounted() {
Bus.$on('on', (msg)=> {
this.message=msg
})
}
}
Vue与AngularJS的区别
Vue与React的区别
vuex的使用借助官方提供的一张图来说明:
image
Vuex有5种属性: 分别是 state、getter、mutation、action、module;
state
Vuex 使用单一状态树,即每个应用将仅仅包含一个store 实例,但单一状态树和模块化并不冲突。存放的数据状态,不可以直接修改里面的数据
mutations
mutations定义的方法动态修改Vuex 的 store 中的状态或数据。
getters
类似vue的计算属性,主要用来过滤一些数据
action
一般面试官问到这里vue基本知识就差不多了, 如果更深入的研究就是和你探讨关于vue的底层源码;或者是具体在项目中遇到的问题,下面列举几个项目中可能遇到的问题:
computed:
watch:
小结:
利用Object.defineProperty劫持对象的访问器,在属性值发生变化时我们可以获取变化,然后根据变化进行后续响应,在vue3.0中通过Proxy代理对象进行类似的操作。
// 这是将要被劫持的对象
const data={
name: '',
};
function say(name) {
if (name==='古天乐') {
console.log('给大家推荐一款超好玩的游戏');
} else if (name==='渣渣辉') {
console.log('戏我演过很多,可游戏我只玩贪玩懒月');
} else {
console.log('来做我的兄弟');
}
}
// 遍历对象,对其属性值进行劫持
Object.keys(data).forEach(function(key) {
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function() {
console.log('get');
},
set: function(newVal) {
// 当属性值发生变化时我们可以进行额外操作
console.log(`大家好,我系${newVal}`);
say(newVal);
},
});
});
data.name='渣渣辉';
//大家好,我系渣渣辉
//戏我演过很多,可游戏我只玩贪玩懒月
Vue 采用数据劫持结合发布—订阅模式的方法,通过 Object.defineProperty() 来劫持各个属性的 setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
image
Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁,主要做的事情
Vue3.x响应式数据原理
Vue3.x改用Proxy替代Object.defineProperty。因为Proxy可以直接监听对象和数组的变化,并且有多达13种拦截方法。并且作为新标准将受到浏览器厂商重点持续的性能优化。
Proxy只会代理对象的第一层,那么Vue3又是怎样处理这个问题的呢?
判断当前Reflect.get的返回值是否为Object,如果是则再通过reactive方法做代理, 这样就实现了深度观测。
监测数组的时候可能触发多次get/set,那么如何防止触发多次呢?
我们可以判断key是否为当前被代理对象target自身属性,也可以判断旧值与新值是否相等,只有满足以上两个条件之一时,才有可能执行trigger
v-model本质上是语法糖,v-model在内部为不同的输入元素使用不同的属性并抛出不同的事件
所以我们可以v-model进行如下改写:
<input v-model="sth" />
// 等同于
<input :value="sth" @input="sth=$event.target.value" />
//Parent
<template>
{{num}}
<Child v-model="num">
</template>
export default {
data(){
return {
num: 0
}
}
}
//Child
<template>
<div @click="add">Add</div>
</template>
export default {
props: ['value'],
methods:{
add(){
this.$emit('input', this.value + 1)
}
}
}
scoped虽然避免了组件间样式污染,但是很多时候我们需要修改组件中的某个样式,但是又不想去除scoped属性
//Parent
<template>
<div class="wrap">
<Child />
</div>
</template>
<style lang="scss" scoped>
.wrap /deep/ .box{
background: red;
}
</style>
//Child
<template>
<div class="box"></div>
</template>
//Parent
<template>
<div class="wrap">
<Child />
</div>
</template>
<style lang="scss" scoped>
//其他样式
</style>
<style lang="scss">
.wrap .box{
background: red;
}
</style>
//Child
<template>
<div class="box"></div>
</template>
Computed本质是一个具备缓存的watcher,依赖的属性发生变化就会更新视图。适用于计算比较消耗性能的计算场景。当表达式过于复杂时,在模板中放入过多逻辑会让模板难以维护,可以将复杂的逻辑放入计算属性中处理
image
<template>{{fullName}}</template>
export default {
data(){
return {
firstName: 'xie',
lastName: 'yu fei',
}
},
computed:{
fullName: function(){
return this.firstName + ' ' + this.lastName
}
}
}
Watch没有缓存性,更多的是观察的作用,可以监听某些数据执行回调。当我们需要深度监听对象中的属性时,可以打开deep:true选项,这样便会对对象中的每一项进行监听。这样会带来性能问题,优化的话可以使用字符串形式监听,如果没有写到组件中,不要忘记使用unWatch手动注销
image
<template>{{fullName}}</template>
export default {
data(){
return {
firstName: 'xie',
lastName: 'xiao fei',
fullName: 'xie xiao fei'
}
},
watch:{
firstName(val) {
this.fullName=val + ' ' + this.lastName
},
lastName(val) {
this.fullName=this.firstName + ' ' + val
}
}
}
导航守卫 router.beforeEach 全局前置守卫
// main.js 入口文件
import router from './router'; // 引入路由
router.beforeEach((to, from, next)=> {
next();
});
router.beforeResolve((to, from, next)=> {
next();
});
router.afterEach((to, from)=> {
console.log('afterEach 全局后置钩子');
});
路由独享的守卫 你可以在路由配置上直接定义 beforeEnter 守卫
const router=new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next)=> {
// ...
}
}
]
})
组件内的守卫你可以在路由组件内直接定义以下路由导航守卫
const Foo={
template: `...`,
beforeRouteEnter (to, from, next) {
// 在渲染该组件的对应路由被 confirm 前调用
// 不!能!获取组件实例 `this`
// 因为当守卫执行前,组件实例还没被创建
},
beforeRouteUpdate (to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 可以访问组件实例 `this`
},
beforeRouteLeave (to, from, next) {
// 导航离开该组件的对应路由时调用,我们用它来禁止用户离开
// 可以访问组件实例 `this`
// 比如还未保存草稿,或者在用户离开前,
将setInterval销毁,防止离开之后,定时器还在调用。
}
}
当时的思路是头部(Header)一般分为左、中、右三个部分,分为三个区域来设计,中间为主标题,每个页面的标题肯定不同,所以可以通过vue props的方式做成可配置对外进行暴露,左侧大部分页面可能都是回退按钮,但是样式和内容不尽相同,右侧一般都是具有功能性的操作按钮,所以左右两侧可以通过vue slot插槽的方式对外暴露以实现多样化,同时也可以提供default slot默认插槽来统一页面风格
Proxy的优势如下:
Object.defineProperty的优势如下:
兼容性好,支持IE9
image
响应式系统简述:
现代前端框架有两种方式侦测变化,一种是pull一种是push
考点: Vue的变化侦测原理
前置知识: 依赖收集、虚拟DOM、响应式系统
根本原因是Vue与React的变化侦测方式有所不同
diff程可以概括为:oldCh和newCh各有两个头尾的变量StartIdx和EndIdx,它们的2个变量相互比较,一共有4种比较方式。如果4种比较都没匹配,如果设置了key,就会用key进行比较,在比较的过程中,变量会往中间靠,一旦StartIdx>EndIdx表明oldCh和newCh至少有一个已经遍历完了,就会结束比较,这四种比较方式就是首、尾、旧尾新头、旧头新尾.
准确: 如果不加key,那么vue会选择复用节点(Vue的就地更新策略),导致之前节点的状态被保留下来,会产生一系列的bug. 快速: key的唯一性可以被Map数据结构充分利用,相比于遍历查找的时间复杂度O(n),Map的时间复杂度仅仅为O(1).
image
代码层面:
Webpack 层面优化:
nextTick 可以让我们在下次 DOM 更新循环结束之后执行延迟回调,用于获得更新后的 DOM
nextTick主要使用了宏任务和微任务。根据执行环境分别尝试采用
定义了一个异步方法,多次调用nextTick会将方法存入队列中,通过这个异步方法清空当前队列
使用了函数劫持的方式,重写了数组的方法,Vue将data中的数组进行了原型链重写,指向了自己定义的数组原型方法。这样当调用数组api时,可以通知依赖更新。如果数组中包含着引用类型,会对数组中的引用类型再次递归遍历进行监控。这样就实现了监测数组变化。
接口请求一般放在mounted中,但需要注意的是服务端渲染时不支持mounted,需要放到created中
一个组件被复用多次的话,也就会创建多个实例。本质上,这些实例用的都是同一个构造函数。如果data是对象的话,对象属于引用类型,会影响到所有的实例。所以为了保证组件不同的实例之间data不冲突,data必须是一个函数
v-model本质就是一个语法糖,可以看成是value + input方法的语法糖。可以通过model属性的prop和event属性来进行自定义。原生的v-model,会根据标签的不同生成不同的事件和属性
原生事件绑定是通过addEventListener绑定给真实元素的,组件事件绑定是通过Vue自定义的$on实现的
简单说,Vue的编译过程就是将template转化为render函数的过程。会经历以下阶段:
简单来说,diff算法有以下过程
key的作用是尽可能的复用 DOM 元素
加载渲染过程
父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount- >子mounted->父mounted
子组件更新过程
父beforeUpdate->子beforeUpdate->子updated->父updated
父组件更新过程
父 beforeUpdate -> 父 updated
销毁过程
父beforeDestroy->子beforeDestroy->子destroyed->父destroyed
SSR也就是服务端渲染,也就是将Vue在客户端把标签渲染成HTML的工作放在服务端完成,然后再把html直接返回给客户端
SSR有着更好的SEO、并且首屏加载速度更快等优点。不过它也有一些缺点,比如我们的开发条件会受到限制,服务器端渲染只支持beforeCreate和created两个钩子,当我们需要一些外部扩展库时需要特殊处理,服务端渲染应用程序也需要处于Node.js的运行环境。还有就是服务器会有更大的负载需求
编码阶段
SEO优化
打包优化
用户体验
还可以使用缓存(客户端缓存、服务端缓存)优化、服务端开启gzip压缩等。
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。
有 5 种,分别是 state、getter、mutation、action、module
vuex 的 getter 是什么?
vuex 的 mutation 是什么?
vuex 的 action 是什么?
面对复杂的应用程序,当管理的状态比较多时;我们需要将vuex的store对象分割成模块(modules)。
如果请求来的数据不是要被其他组件公用,仅仅在请求的组件内使用,就不需要放入 vuex 的 state 里如果被其他地方复用,请将请求放入 action 里,方便复用,并包装成 promise 返回
将当前组件的<style>修改为<style scoped>
var a=[1,2,3,4]
var b=[1,2,3,4]
delete a[0]
console.log(a) //[empty,2,3,4]
this.$delete(b,0)
console.log(b) //[2,3,4]
可以
<input type="text" :value="name" @input="onInput" @focus="onFocus" @blur="onBlur" />
v-on 常用修饰符
babel-polyfill插件
以下方法调用会改变原始数组:push(), pop(), shift(), unshift(), splice(), sort(), reverse(),Vue.set( target, key, value )
在mounted
注意 mounted 不会承诺所有的子组件也都一起被挂载。如果你希望等到整个视图都渲染完毕,可以用 vm.$nextTick 替换掉 mounted
mounted: function () {
this.$nextTick(function () {
// Code that will run only after the
// entire view has been rendered
})
}
第一次加载会触发哪几个钩子
会触发beforeCreate , created ,beforeMount ,mounted
active classname, isActive 变量
<div :class="{ active: isActive }"></div>
*请认真填写需求信息,我们会在24小时内与您取得联系。