珍藏

175 阅读14分钟

感谢: 作者:神三元 链接:juejin.cn/post/684490… 来源:掘金

感谢: 作者:冴羽 链接:github.com/mqyqingfeng… 来源:github

浏览器

  • 说下http跟https的区别

    • 对称加密 对称加密是最简单的方式,指的是加密和解密用的是同样的密钥;

    如果使用对称加密的方式,那么第三方可以在中间获取到client_random、server_random和加密方法,由于这个加密方法同时可以解密,所以中间人可以成功对暗号进行解密,拿到数据,很容易就将这种加密方式破解了

    • 非对称加密 如果有 A、 B 两把密钥,如果用 A 加密过的数据包只能用 B 解密,反之,如果用 B 加密过的数据包只能用 A 解密;

    如果使用非对称加密。在这种加密方式中,服务器手里有两把钥匙,一把是公钥,也就是说每个人都能拿到,是公开的,另一把是私钥,这把私钥只有服务器自己知道。

    好,现在开始传输。 浏览器把client_random和加密方法列表传过来,服务器接收到,把server_random、加密方法和公钥传给浏览器。

    现在两者拥有相同的client_random、server_random和加密方法。然后浏览器用公钥将client_random和server_random加密,生成与服务器通信的暗号。

    这时候由于是非对称加密,公钥加密过的数据只能用私钥解密,因此中间人就算拿到浏览器传来的数据,由于他没有私钥,照样无法解密,保证了数据的安全性。

    这难道一定就安全吗?聪明的小伙伴早就发现了端倪。回到非对称加密的定义,公钥加密的数据可以用私钥解密,那私钥加密的数据也可以用公钥解密呀!

    服务器的数据只能用私钥进行加密(因为如果它用公钥那么浏览器也没法解密啦),中间人一旦拿到公钥,那么就可以对服务端传来的数据进行解密了,就这样又被破解了。而且,只是采用非对称加密,对于服务器性能的消耗也是相当巨大的,因此我们暂且不采用这种方案。

    • 现在浏览器一般是用什么加密方法?说下为何?

    对称加密和非对称加密的结合,浏览器向服务器发送client_random和加密方法列表。服务器接收到,返回server_random、加密方法以及公钥。

    浏览器接收,接着生成另一个随机数pre_random, 并且用公钥加密,传给服务器。(敲黑板!重点操作!)服务器用私钥解密这个被加密后的pre_random。

    现在浏览器和服务器有三样相同的凭证:client_random、server_random和pre_random。然后两者用相同的加密方法混合这三个随机数,生成最终的密钥。

    然后浏览器和服务器尽管用一样的密钥进行通信,即使用对称加密。这个最终的密钥是很难被中间人拿到的,为什么呢? 因为中间人没有私钥,从而拿不到pre_random,也就无法生成最终的密钥了。

    • 数字证书 尽管通过两者加密方式的结合,能够很好地实现加密传输,但实际上还是存在一些问题。黑客如果采用 DNS 劫持,将目标地址替换成黑客服务器的地址,然后黑客自己造一份公钥和私钥,照样能进行数据传输。而对于浏览器用户而言,他是不知道自己正在访问一个危险的服务器的。 事实上HTTPS在上述结合对称和非对称加密的基础上,又添加了数字证书认证的步骤。其目的就是让服务器证明自己的身份。

    传输过程

    为了获取这个证书,服务器运营者需要向第三方认证机构获取授权,这个第三方机构也叫CA(Certificate Authority), 认证通过后 CA 会给服务器颁发数字证书。 这个数字证书有两个作用:

    服务器向浏览器证明自己的身份。 把公钥传给浏览器。

    这个验证的过程发生在什么时候呢? 当服务器传送server_random、加密方法的时候,顺便会带上数字证书(包含了公钥), 接着浏览器接收之后就会开始验证数字证书。如果验证通过,那么后面的过程照常进行,否则拒绝执行。

现在我们来梳理一下HTTPS最终的加解密过程:

image.png

认证过程

浏览器拿到数字证书后,如何来对证书进行认证呢? 首先,会读取证书中的明文内容。CA 进行数字证书的签名时会保存一个 Hash 函数,来这个函数来计算明文内容得到信息A,然后用公钥解密明文内容得到信息B,两份信息做比对,一致则表示认证合法。

当然有时候对于浏览器而言,它不知道哪些 CA 是值得信任的,因此会继续查找 CA 的上级 CA,以同样的信息比对方式验证上级 CA 的合法性。一般根级的 CA 会内置在操作系统当中,当然如果向上找没有找到根级的 CA,那么将被视为不合法。

  • 三次握手是什么?
    • 为什么要三次握手? juejin.cn/post/693380…
    • 握手时可以携带数据吗?
    • 握手失败后会发生什么?
  • 断开连接的时候四次挥手是什么?
    • 为什么要四次才可以?
    • WAIT_TIME时发生了什么?
    • 等待时间一般多久?为什么?
  • 说下你认为请求头有哪些关键词 +
  • 重排重绘区别?
    • 开发中需要怎么避免?
    • 有何优化方式?
  • 浏览器输入url发生哪些事情?
    • 网络层?
    • 渲染层?
  • xss,csrf攻击分别是什么?
  • 说下缓存的区别?
    • cookie跟storage有什么区别?
    • cookie是浏览器的漏洞吗?它是怎么弥补的?
  • 什么是跨域?
    • 浏览器如何检测跨域? 预检请求

    • 跨域的本质是? 不同源 协议名,域名,端口号不一致都是跨域 cookie中的domin不一致请求接口的时候不会携带

    • jsonp为何不携带cookie信息? 因为你请求的是另一个域名下的接口 只会携带另一个域名下的cookie

js

  • 0.1+0.2 === 0.3吗?最大安全数字是?
  • eventLoop机制?
    • 宏任务
    • 微任务
    • 给个事件循环代码,写出答案
  • new一个函数的时候发生了什么?
  • symbol有什么好处
  • this指向,给段代码给出输出
  • 什么是原型链? juejin.cn/post/684490…
  • 实现一个继承
  • 什么是原型链?
当对象查找一个属性的时候,如果没有在自身找到,
那么就会查找自身的原型,如果原型还没有找到,
那么会继续查找原型的原型,直到找到 Object.prototype 的原型时,
此时原型为 null,查找停止。 这种通过 通过原型链接的逐级向上的查找链被称为原型链
  • 什么是原型继承?
一个对象可以使用另外一个对象的属性或者方法,就称之为继承。
具体是通过将这个对象的原型设置为另外一个对象,这样根据原型链的规则,
如果查找一个对象属性且在自身不存在时,就会查找另外一个对象,
相当于一个对象可以使用另外一个对象的属性和方法了。
  • 如果一个构造函数,bind了一个对象,用这个构造函数创建出的实例会继承这个对象的属性吗?为什么?
  • bind,call,apply
  • 隐式转换,显示转换
  • promise原理,all,rise
class MyPromise {
  constructor(fn) {
    this.resolvedCallbacks = [];
    this.rejectedCallbacks = [];
    
    this.state = 'PENDING';
    this.value = '';
    
    fn(this.resolve.bind(this), this.reject.bind(this));
    
  }
  
  resolve(value) {
    if (this.state === 'PENDING') {
      this.state = 'RESOLVED';
      this.value = value;
      
      this.resolvedCallbacks.map(cb => cb(value));   
    }
  }
  
  reject(value) {
    if (this.state === 'PENDING') {
      this.state = 'REJECTED';
      this.value = value;
      
      this.rejectedCallbacks.map(cb => cb(value));
    }
  }
  
  then(onFulfilled, onRejected) {
    if (this.state === 'PENDING') {
      this.resolvedCallbacks.push(onFulfilled);
      this.rejectedCallbacks.push(onRejected);
      
    }
    
    if (this.state === 'RESOLVED') {
      onFulfilled(this.value);
    }
    
    if (this.state === 'REJECTED') {
      onRejected(this.value);
    }
  }
}

promise的catch为什么要写在最后面而不经常使用promise的第二个参数,因为使用第二个参数 不能捕获到内层的函数报错信息

// bad
somethingAync.then(function() {
    return somethingElseAsync();
}, function(err) {
    handleMyError(err);
});
如果 somethingElseAsync 抛出错误,是无法被捕获的。你可以写成:
// good
somethingAsync()
.then(function() {
    return somethingElseAsync();
})
.catch(function(err) {
    handleMyError(err);
});
  • async await
  • 如何判断数组
  • 如何判读一个对象是不是空对象
  • 事件监听,冒泡false,捕获true
  • 闭包是什么?怎么出现闭包?
  • 数组有哪些函数?
  • 用过TS吗?举例
  • 箭头函数和普通函数的区别?
  • 知道es6的class吗?static关键字吗?
  • module.export;export.default;export有哪些区别?
  • 弹层穿透问题解决?
思路:首先确定
  • 写个深拷贝
  • 自己写个rem适配
// 按照 375的切图来的
 function calcRem() {
  var deviceWidth = document.documentElement.clientWidth;
  if (deviceWidth > 640) {
    deviceWidth = 640;
  } else if (deviceWidth < 320) {
    deviceWidth = 320;
  }
  if (navigator.userAgent && navigator.userAgent.indexOf('iPad') > -1) {
    deviceWidth = document.documentElement.clientWidth;
  }
  document.documentElement.style.fontSize = deviceWidth / 3.75 + 'px';
}
calcRem();
window.onresize = function () {
  calcRem()
}
  • 实现数组扁平化
export const flattenDeep = arr1 => {
  return arr1.reduce((acc, val) => Array.isArray(val) ? acc.concat(flattenDeep(val)) : acc.concat(val), []);
}
  • 数组去重
Array.from(new set([1,1,2,3,4,2]))
[...new set([1,1,2,3,4,2])]
  • 实现柯里化 柯里化是什么?是指一个函数,它接收函数A,能返回一个新的函数,新函数能够处理函数A的剩余参数 为什么要有柯里化?什么场景需要? github.com/mqyqingfeng…
  1. 先看一个简单版本,这个版本仅return一次函数,所以他的参数传递最多两次必须要传完,想要使用 curry(add)(1)(2)会报错。
      var curry = function (fn) {
        var args = [].slice.call(arguments, 1); // curry调用时的参数
        return function () {
          var newArgs = args.concat([].slice.call(arguments)); // addCurry调用时的参数
          console.log(newArgs)
          return fn.apply(this, newArgs);
        };
      };
      function add(a, b) {
          return a + b;
        }
    
        var addCurry = curry(add, 1, 2);
        // addCurry() // 3
        console.log(addCurry())
        //或者
        var addCurry1 = curry(add, 1);
        console.log(addCurry1(2))
        //或者
        var addCurry2 = curry(add);
        console.log(addCurry2(1, 2)) // 3
    
  2. 优化
  // 第二版
function sub_curry(fn) {
  var args = [].slice.call(arguments, 1);
  return function () {
    return fn.apply(this, args.concat([].slice.call(arguments)));
  };
}

function curry(fn, length) {

  length = length || fn.length;

  var slice = Array.prototype.slice;

  return function () {
    if (arguments.length < length) { // 确保剩下最后一个参数的话 可以直接走sub_curry的拼接
      var combined = [fn].concat(slice.call(arguments));
      return curry(sub_curry.apply(this, combined), length - arguments.length);
    } else {
      console.log(fn) //输出 fn.apply(this, args.concat([].slice.call(arguments))); 根据执行函数fn 运算拼接参数的逻辑
      return fn.apply(this, arguments);
    }
  };
}
var fn = curry(function add(a, b) {
    return a + b;
  }) 
const one = fn(1)(2)
console.log(one) // 3
  • 变量提升
var a
function aa(){
  a = 20
  console.log(a)
}
aa() // 20
  • instanceof
用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上
function Car(make, model, year) {
  this.make = make;
  this.model = model;
  this.year = year;
}
const auto = new Car('Honda', 'Accord', 1998);

console.log(auto instanceof Car);
// expected output: true

console.log(auto instanceof Object);
// expected output: true

  • typeOf
返回一个字符串,表示未经计算的操作数的类型;返回值可能有 string boolean number function undefined symbol object
console.log(typeof 42);
// expected output: "number"

console.log(typeof 'blubber');
// expected output: "string"

console.log(typeof true);
// expected output: "boolean"

console.log(typeof undeclaredVariable);
// expected output: "undefined"

  • 节流
核心思想: 如果在定时器的时间范围内再次触发,则不予理睬,等当前定时器完成,才能启动下一个定时器任务。
比如:等公交,公交不到点,再怎么嚷嚷都不行。
闭包
function throttle(fn, interval) {
  let flag = true;
  return function(...args) {
    let context = this;
    if (!flag) return;
    flag = false;
    setTimeout(() => {
      fn.apply(context, args);
      flag = true;
    }, interval);
  };
};
const throttle = function(fn, interval) {
  let last = 0;
  return function (...args) {
    let context = this;
    let now = +new Date();
    // 还没到时间
    if(now - last < interval) return;
    last = now;
    fn.apply(this, args)
  }
}

function throttle(fn, delay) {
    var timer;
    return function () {
        var _this = this;
        var args = arguments;
        if (timer) {
            return;
        }
        timer = setTimeout(function () {
            fn.apply(_this, args);
            timer = null; // 在delay后执行完fn之后清空timer,此时timer为假,throttle触发可以进入计时器
        }, delay)
    }
}
  • 防抖
核心思想: 每次事件触发则删除原来的定时器,建立新的定时器。以最后一次触发为准
闭包
function debounce(fn, delay) {
  let timer = null;
  return function (...args) { // ...args其实就是把调用的参数解构
    let context = this;
    if(timer) clearTimeout(timer);
    timer = setTimeout(function() {
      fn.apply(context, args); // apply后面跟的是数组 其实args就是arguments是个数组
    }, delay);
  }
}

function debounce(fn, delay) {
    var timer; // 维护一个 timer
    return function () {
        var _this = this; // 取debounce执行作用域的this
        var args = arguments;
        if (timer) {
            clearTimeout(timer);
        }
        timer = setTimeout(function () {
            fn.apply(_this, args); // 用apply指向调用debounce的对象,相当于_this.fn(args);
        }, delay);
    };
}
  • 加强版节流
防抖有时候触发的太频繁会导致一次响应都没有,我们希望到了固定的时间必须给用户一个响应,事实上很多前端库就是采取了这样的思路。
function throttle(fn, delay) {
  let last = 0, timer = null;
  return function (...args) {
    let context = this;
    let now = new Date();
    if(now - last < delay){
      clearTimeout(timer);
      setTimeout(function() {
        last = now;
        fn.apply(context, args);
      }, delay);
    } else {
      // 这个时候表示时间到了,必须给响应
      last = now;
      fn.apply(context, args);
    }
  }
}
  • for in与for of与foreach的区别?哪个可以使用break,return跳出
for of 可以跳出
foreach不能跳出
for in 遍历对象 只能获取对象的键名,不能拿到值
for of 可以获取到值
总之,for...in 循环主要是为了遍历对象而生,不适用于遍历数组
  • bind,call,apply的区别?
bind强指向
bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用
call传递多个参数 可以实现继承
function Product(name, price) {
  this.name = name;
  this.price = price;
}

function Food(name, price) {
  Product.call(this, name, price); // 谁主导Product.call,Product主导所以用Product的属性
  this.category = 'food';
}

console.log(new Food('cheese', 5).name);
// expected output: "cheese"
apply传递一个数组

  • setTimeout(fn, 0)多久才执行
setTimeout 按照顺序放到队列里面,然后等待函数调用栈清空之后才开始执行,而这些操作进入队列的顺序,则由设定的延迟时间来决定

css

  • 两个div 都写margin 会发生什么?
    • 为什么会折叠?
    • 如何避免?
    • BFC是什么?它是css的一个bug吗?
       
    
  • 怎么实现上中下布局,中间部分自适应高度?
  • 实现一个盒子自动居中,不知宽度高度如何实现?
  • flex的三个属性?flex-basic主要做什么?
  • 画个一像素的边框
  • 文字上下居中
  • 实现9px的字号
    scale缩放
    

vue

  • 生命周期

  • 父子组件通信?

    • 父子组件通信
      • 父->子props,子->父 onon、emit
      • 获取父子组件实例 parentparent、children
      • Ref 获取实例的方式调用组件的属性或者方法
      • Provide、inject 官方不推荐使用,但是写组件库时很常用
    • 兄弟组件通信
      • Event Bus 实现跨组件通信 Vue.prototype.$bus = new Vue
      • Vuex
    • 跨级组件通信
      • Vuex
      • attrsattrs、listeners
      • Provide、inject
  • slot好处

  • MVVM数据劫持&监听

    • 2.0

    Vue在初始化数据时,会使用Object.defineProperty重新定义data中的所有属性,当页面使用对应属性时,首先会进行依赖收集(收集当前组件的watcher)如果属性发生变化会通知相关依赖进行更新操作(发布订阅)。

    • 3.0

    Vue3.x改用Proxy替代Object.defineProperty。因为Proxy可以直接监听对象和数组的变化,并且有多达13种拦截方法。并且作为新标准将受到浏览器厂商重点持续的性能优化。

    Proxy只会代理对象的第一层,那么Vue3又是怎样处理这个问题的呢?

    判断当前Reflect.get的返回值是否为Object,如果是则再通过reactive方法做代理, 这样就实现了深度观测

    监测数组的时候可能触发多次get/set,那么如何防止触发多次呢?

    我们可以判断key是否为当前被代理对象target自身属性,也可以判断旧值与新值是否相等,只有满足以上两个条件之一时,才有可能执行trigger

    再说一下vue2.x中如何监测数组变化

    使用了函数劫持的方式,重写了数组的方法,Vue将data中的数组进行了原型链重写,指向了自己定义的数组原型方法。这样当调用数组api时,可以通知依赖更新。如果数组中包含着引用类型,会对数组中的引用类型再次递归遍历进行监控。这样就实现了监测数组变化。

  • nextTick知道吗,实现原理是什么?

在下次 DOM 更新循环结束之后执行延迟回调。nextTick主要使用了宏任务和微任务。根据执行环境分别尝试采用

Promise;MutationObserver;setImmediate如果以上都不行则采用setTimeout 定义了一个异步方法,多次调用nextTick会将方法存入队列中,通过这个异步方法清空当前队列。

  • VNode是什么?

  • Data为什么是个函数?对象是引用类型

  • v-if和v-show区别

  • computed和watch区别?

  • v-model原理

  • vue模板编译原理?

  • 遍历时为何要加key?

    • diff算法 vue2.0 和 3.0的区别?
    • 为什么不是一个一个遍历而是先要头尾比较?
    • 3.0的主要算法是?为什么要使用最长递增子序列?
  • 虚拟Dom和key的作用?

  • keep-alive

  • Vue中组件生命周期调用顺序说一下

    组件的调用顺序都是先父后子,渲染完成的顺序是先子后父。组件的销毁操作是先父后子,销毁完成的顺序是先子后父。

  • SSR了解吗 SSR也就是服务端渲染,也就是将Vue在客户端把标签渲染成HTML的工作放在服务端完成,然后再把html直接返回给客户端。 SSR有着更好的SEO、并且首屏加载速度更快等优点。不过它也有一些缺点,比如我们的开发条件会受到限制,服务器端渲染只支持beforeCreate和created两个钩子,当我们需要一些外部扩展库时需要特殊处理,服务端渲染应用程序也需要处于Node.js的运行环境。还有就是服务器会有更大的负载需求

  • 实现一个插件

  • 实现一个组件

  • 怎么实现页面缓存?vuex有何弊端?接入外部三方需要考虑哪些问题?

  • vue跟jquery的区别

  • bable stages 有几个阶段?bable是干嘛的?

  • 配置路由 history跟hash有啥区别? location.hash的值实际就是URL中#后面的东西。 history实际采用了HTML5中提供的API来实现,主要有history.pushState()和history.replaceState()。

    • 又因为路径遇到什么问题吗?(访问空白)

    • 怎么解决?

webpack

  • 用了哪些loader?

    1. vue-loader: 把vue转成js
    2. sass-loader: 把sass转成css
    3. css-loader: 把css转成js
    4. style-loader: 把css注入到js里,通过dom操作
    5. stylus-loader:加载并编译stylus文件
    6. file-loader: 把文件输出到文件夹中使用相对路径引用
    7. url-loader: 与file-loader类似,唯一不同的是可以添加阈值,超过limit阈值生成带有hash后缀的文件,不超过阈值的转化成base64
    8. image-loader:加载并压缩文件
    9. bable-loader:es6转成es5
    10. ts-loader:ts转成js
    11. eslint-loader:eslint校验
    12. html-loader: 更多可查看官方文档:webpack.docschina.org/loaders/
  • 用了哪些plugin:

    1. html-webpack-plugin:生成html文件
    2. terser-webpack-plugin: 压缩js
    3. define-plugin: 允许编译的时候配置全局变量;比如版本号
    4. HotModuleReplacementPlugin:启用热更新
    5. mini-css-extract-plugin:css提取为独立文件
    6. ignore-plugin:忽略部分文件
    7. CommonsChunkPlugin 插件可以将公共的依赖模块提取到已有的入口 chunk 中,或者提取到一个新生成的 chunk。
    8. UglifyJsPlugin使用
  • 说下plugin和loader的区别

    1. loader是函数,转化为webpack支持的语言js,比如css->js注入dom中;在module.rules 中配置,从后向前编译
        module.exports = {
          module: {
            rules: [
              {
                test: /\.css$/,
                use: [
                  // [style-loader](/loaders/style-loader)
                  { loader: 'style-loader' },
                  // [css-loader](/loaders/css-loader)
                  {
                    loader: 'css-loader',
                    options: {
                      modules: true
                    }
                  },
                  // [sass-loader](/loaders/sass-loader)
                  { loader: 'sass-loader' }
                ]
              }
            ]
          }
        }
    
    1. plugin是插件,可以拓展weboack的功能,使用webpack的钩子函数,监听webpack的生命周期事件,在合适的情况下添加逻辑,比如可以把css单独提取出来作为css文件;在plugins中单独配置,数组,每个里面new
    const CompressionPlugin = require("compression-webpack-plugin");
    
    module.exports = {
      plugins: [new CompressionPlugin()],
    };
    And run webpack via your preferred method.
    
  • 为什么要用webpack

    1. 再没有webpack之前,使用js需要全部写在html的script标签里面,涉及到先后加载顺序
    2. 全局变量的污染,后来使用了自执行函数,闭包
    3. 改动一处,所有使用到的都会编译,不会按需加载
  • webpack的工作流程是什么?

    1. 初始化:读取配置,加载plugin,实例化compiler
    2. 编译:从entry出发,针对Module串行调用loader翻译文件的内容,如果该Module还有依赖的Module,递归的编译处理
    3. 输出:将编译之后的Module组成chunk,将chunk转换成文件,输出到文件系统里。
  • 模块打包原理?

    webpack为每个文件提供一个导入导出环境,不影响原来的代码逻辑跟顺序

  • 文件监听原理?

    当代码发生改变的时候,自动构建新的输出文件; 轮训判断是否改变,如果某个文件发生改变,不会立刻告诉监听器,先会缓存起来,等到aggregateTimeout时间后再改变。 确定是:需要刷新浏览器手动刷新 两种方式:

    1. 执行命令的时候 加上--watch(vue-cli使用的是这种)
    2. 配置里添加上 watch:true
    module.export = {   
    // 默认false,也就是不开启    
    watch: true,    
    // 只有开启监听模式时,watchOptions才有意义    
    watchOptions: {
    // 默认为空,不监听的文件或者文件夹,支持正则匹配             
    ignored: /node_modules/,
    // 监听到变化发生后会等300ms再去执行,默认300ms
    aggregateTimeout:300, 
    // 判断文件是否发生变化是通过不停询问系统指定文件有没有变化实现的,默认每秒问1000次        
    poll:1000 
    }}
    
  • 说下webpack的热更新原理? Webpack 的热更新又称热替换(Hot Module Replacement),缩写为 HMR。 这个机制可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块。

vue文件 保存时 浏览器不刷新,页面改动的话,拿到带有hash的文件:

具体流程如下:

以下截图为send页面,改动页面时请求的数据,当本地资源发生变化时,实际上 webpack-dev-server 与浏览器之间维护了一个 Websocket,webpack-dev-server 会向浏览器推送更新,并带上构建时的 hash,让客户端与上一次资源进行对比,改变的话就把当前请求生成一个hash,客户端比对发现差异之后,会向webpack-dev-server发起ajax再次请求带有hash后缀的文件,下次再改变的时候生成的send文件加上该hash后缀。

当 HMR 失败后,回退到 live reload 操作,也就是进行浏览器刷新来获取最新打包代码。这又是我们项目里最常见的之前的业务弹层都没了 直接刷新当前页面。 image.png

  • 文件指纹是什么?怎么用?

文件指纹就是文件的后缀

  1. hash:和整个项目的构建相关,只要项目文件有修改,整个项目构建的 hash 值就会更改,常见于图片,文件后缀的hash构建

vue-cli是asset文件下都用hash;图片,svg,media,fonts;路径packages/@vue/cli-service/lib/config/assets.js

image.png 2. contenthash:根据文件内容来定义hash,contenthash就更新;常用于css, js

Vue-cli源码打包

image.png image.png image.png

image.png

image.png

  1. Chunkhash:根据webpack打包的chunk有关,不同的entry会生成不同的chunkhash,常用于output里的filename输出
module.exports = {
    entry: {        
        app: './scr/app.js',        
        search: './src/search.js'    
    },    
    output: {        
        filename: '[name][chunkhash:8].js',        
        path:__dirname + '/dist'    
    },    
    plugins:[        
        new MiniCssExtractPlugin({            
        filename: `[name][contenthash:8].css`       
      })    
    ]}
  1. 使用高版本的webpack和nodejs
  2. 较新的版本能够建立更高效的模块树以及提高解析速度
  3. 多进程构建:thread-loader
    if (useThreads) {
      addLoader({
        name: 'thread-loader',
        loader: require.resolve('thread-loader'),
        options:
          typeof projectOptions.parallel === 'number'
            ? { workers: projectOptions.parallel }
            : {}
      })
    }
  1. include,exclude 缩小loader范围
  2. noParse 对完全不需要解析的库进行忽略 webpackConfig.module.noParse(/^(vue|vue-router|vuex|vuex-router-sync)$/)
  3. 合理使用alias 一般都只对src 最外层设置别名
webpackConfig.module..alias.set('@', api.resolve('src'))
  1. 使用html-webpack-externals-plugin时,将基础服务使用cdn引用,比如wx相关的cdn引入
  2. 提取公共代码时 使用SplitChunksPlugin替代commonChunkPlugin
packages/@vue/cli-service/lib/config/app.js
    // vue-cli的源码 code splitting
    if (process.env.NODE_ENV !== 'test') {
      webpackConfig.optimization.splitChunks({
        cacheGroups: {
          defaultVendors: {
            name: `chunk-vendors`, // 使用三方的
            test: /[\\/]node_modules[\\/]/,
            priority: -10,
            chunks: 'initial'
          },
          common: {
            name: `chunk-common`, // 公共的
            minChunks: 2,
            priority: -20,
            chunks: 'initial',
            reuseExistingChunk: true
          }
        }
      })
    }

image.png

  1. treeShaking 标记是否有副作用,移除未引用的上下文,配合UglifyJsPlugin使用
  2. 将第三方库(library)(例如 lodash)提取到单独的 vendor chunk 文件中,因为他们不会频繁的改动,所以放在缓存里比较好
  3. 使用HashedModuleIdsPlugin防止vender里面因为module.id改变而导致hash改变。达到缓存的效果
  4. 使用 DllPlugin 将更改不频繁的代码进行单独编译
  5. 使用 cache-loader 启用持久化缓存。使用 package.json 中的 "postinstall" 清除缓存目录。
    addLoader({
      name: 'cache-loader',
      loader: require.resolve('cache-loader'),
      options: api.genCacheConfig('ts-loader', {
        'ts-loader': require('ts-loader/package.json').version,
        'typescript': require('typescript/package.json').version,
        modern: !!process.env.VUE_CLI_MODERN_BUILD
      }, 'tsconfig.json')
    })
  1. terser-webpack-plugin/uglify-webpack-plugin 代码压缩
    if (process.env.NODE_ENV === 'production') {
      const TerserPluginV4 = require('terser-webpack-plugin')
      config.optimization.minimizer('terser').init(
        (Plugin, [terserPluginOptions]) =>
          new TerserPluginV4({
            sourceMap: rootOptions.productionSourceMap,
            cache: true,
            ...terserPluginOptions
          })
      )
  1. mini-css-extract-plugin 提取css文件,通过 css-loader 的 minimize 选项开启 cssnano 压缩 CSS
// inject CSS extraction plugin 注入提取css插件
if (shouldExtract) {
      webpackConfig
        .plugin('extract-css')
          .use(require('mini-css-extract-plugin'), [extractOptions])

      // minify extracted CSS 压缩提取的css
      webpackConfig.optimization
        .minimizer('css')
          .use(require('css-minimizer-webpack-plugin'), [{
            parallel: rootOptions.parallel, // 多进程并行压缩
            sourceMap: rootOptions.productionSourceMap && sourceMap,
            minimizerOptions: cssnanoOptions
          }])
    }
  1. 图片资源处理。base64
  const inlineLimit = 4096

  const vueMajor = getVueMajor(api.getCwd())
  const supportsEsModuleAsset = (vueMajor !== 2)

  const genAssetSubPath = dir => {
    return getAssetPath(
      options,
      `${dir}/[name]${options.filenameHashing ? '.[hash:8]' : ''}.[ext]`
    )
  }
  const genUrlLoaderOptions = dir => {
    return {
      limit: inlineLimit,
      esModule: supportsEsModuleAsset,
      // use explicit fallback to avoid regression in url-loader>=1.1.0
      fallback: {
        loader: require.resolve('file-loader'),
        options: {
          name: genAssetSubPath(dir),
          esModule: supportsEsModuleAsset
        }
      }
    }
  }

  api.chainWebpack(webpackConfig => {
    webpackConfig.module
      .rule('images')
        .test(/\.(png|jpe?g|gif|webp)(\?.*)?$/)
        .use('url-loader')
          .loader(require.resolve('url-loader'))
          .options(genUrlLoaderOptions('img'))

    // do not base64-inline SVGs.
    // https://github.com/facebookincubator/create-react-app/pull/1180
    webpackConfig.module
      .rule('svg')
        .test(/\.(svg)(\?.*)?$/)
        .use('file-loader')
          .loader(require.resolve('file-loader'))
          .options({
            name: genAssetSubPath('img'),
            esModule: supportsEsModuleAsset
          })

    webpackConfig.module
      .rule('media')
        .test(/\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/)
        .use('url-loader')
          .loader(require.resolve('url-loader'))
          .options(genUrlLoaderOptions('media'))

    webpackConfig.module
      .rule('fonts')
        .test(/\.(woff2?|eot|ttf|otf)(\?.*)?$/i)
        .use('url-loader')
          .loader(require.resolve('url-loader'))
          .options(genUrlLoaderOptions('fonts'))
  })
  • 代码分隔的本质是什么 (splitChunksPlugin,commonChunkPlugin) 在源代码和打包一个文件之间找个平衡,在服务器能承受的压力下达到更好的用户体验。

    其实就是按需加载 ,三方的依赖js打包成vendor.hash[8].js 搞成缓存 使用HashedModulePlus记住id不改变,公共的组件打包成common.hash[8].js

    其他的尽量按需加载,按照模块放在一个chunkName里面

    • 如果直接源代码的话,请求太多,图片&js都算
    • 如果直接打包成一个js文件 代码量过多,白屏时间更长
  • 有自己写过plugin吗? 没有 但是可以参考vue-cli里的packages/@vue/cli-service/lib/webpack/ModernModePlugin.js 具体可看官网Plugin API image.png

  • 自己写过loader吗? 具体可看官网loader API

小程序

  • 小程序内嵌h5 怎么实现登录打通?
  • 小程序分包机制?
  • 小程序跳转
  • 小程序自动化构建CICD

Node

项目

  • 优化手段 从浏览器的整个过程来看 有三个方面

    1. 网络请求阶段
      1. 首先要对接口进行封装,不要频繁的请求接口
      2. 防抖节流
    2. 渲染阶段
      1. 避免重排和重绘,写样式的时候尽量,尽量使用class,添加html放在一个块里。
      2. for循环渲染时加key唯一标识
      3. 页面使用keep-alive缓存组件状态
      4. 使用v-show替代v-if
      5. 长列表滚动到可视区域懒加载图片
      6. 预渲染服务端渲染SSR
      7. css不要太多层级嵌套 最好不要超过3层
    3. 打包阶段
      1. 压缩包体积

      2. webpack打包分模块,懒加载模块webpackChunkName

      3. 提高打包效率:noParse过滤不需要解析的文件,比如打包的时候依赖了三方库

      4. Tree Shaking 去掉不需要的代码:通过副作用标识需要导入,有没有副作用又没有导入的就删除,或者加上pure纯净标识,可以直接删除文件

      5. 使用cdn加载第三方模块

      6. 多线程打包happypacksplitChunks

      7. 抽离公共文件

    • SEO优化 预渲染服务端渲染SSR
    • 打包优化

    压缩代码Tree Shaking/Scope Hoisting 使用cdn加载第三方模块 多线程打包happypacksplitChunks 抽离公共文件 sourceMap优化

    • 用户体验

      1. 骨架屏
      2. PWA:渐进式web应用
      3. 还可以使用缓存(客户端缓存、服务端缓存)优化、服务端开启gzip压缩等。
  • 是否做过重构?思路是? 思路是先边缘后中心迁移,涉及到项目迭代同步更新,最好是跟业务商量好,能宽限几天,实在不行的,当时我们是两队人马,即写业务又要重构。 重构小程序 Taro重写,因为我们要做百度小程序和京东小程序,所以必须一步转换比较快些,迁移是个长期的过程,人员有限,只能先保微信小程序,因为有的业务逻辑百度跟京东并不需要,而且京东小程序是20年刚介入的,他们的底层还不太完善,所以有的组件或者属性也不支持,所以互相理解吧 最终我理解的重构应该是一端代码生成多端,共同维护在一个代码分支上。

  • 遇到哪些印象深刻或者难以攻克的问题 使用技术方案解决业务难题

1. 小v寄

时间紧张任务重,所以考虑h5实现,并且多端小程序嵌入h5, 难题:

  1. 2周的时间开发完成并且联调,因为有多端,所以考虑h5,并对项目做涉及,目前先放在京东快递项目里面,但是为了方便以后单独拿出去,单写一个模块,mock数据也可以使用上,网络请求也可以单独封装一套,统一处理。

  2. 需要自己封装日历插件,考虑跟后端接口请求如何最优。思考是前端传日历上起始终止时间好还是传月份好,希望后端给到的正好是跟前端一一对应的数据,这样前端不需要再次遍历,只需要查找42次(最多7*6)对上即可,如果后端仅返回有下单数量的对象,那就需要两次for循环最多42的平方。

  3. h5里面涉及到store里面的数据清除问题,需要在mouted钩子函数之前拿到标识(以下是解决思路)

    • 对外赋能,根据source来源下单埋点,并且是否知道是否使用jssdk 还是只需要纯净版,挂载到vue上
    • 随便一个页面都可以给别人直接使用,涉及到全局状态state的变量值赋值问题如何解决是个难题:
      1. 使用query后面带上参数,在beforeRouter里面进行赋值操作,判断from 再进行state赋值,因为query会一直在,跳转别的页面再回来是携带的 不能每次都进行state赋值,要有选择性的赋值;注意query上不能携带太长数据,容易被截取,可以到页面之后再请求数据,再给state赋值
      2. 先跳转一个空白页面在空白页面里面写逻辑进行赋值,再分发跳转
      3. 或者再mouted挂载App的时候,在路由守卫里面添加一个新的变量,到页面里面根据新的变量做判断
      4. 存缓存里面,但是缓存什么时候该清什么时候不清是需要注意的点
  4. 小程序打开的 需要在查看订单的时候跳回到京东快递小程序并且兼容其他的小程序,比如京动购物小程序不会有问题

  5. 如何使用12px的字体;做优惠券规则列表的时候,由于对字号有特别要求,所以整体采用scale进行缩放实现

  6. 追悼会需要置灰所以使用css属性filter:grayscale(100%)

2.京东快递h5

  1. 接入招商银行的时候 使用pageshow 解决回退不刷新问题
     window.addEventListener('pageshow', e => { // wkWebView 回退不刷新问题
      if (e.persisted) { // 如果从缓存里面取 返回true
        window.location.reload();
      }
    });
    

3.一件代发

  1. 怎么在接口调用之前把用户是否登录通知各页面? 页面挂载的时候,调取接口对store里的state赋值。因为落地页不用管是否登录都一样,所以可行;

  2. 进入首页需要知道角色 判断展示页面? 设置一个变量 有三个状态值 -1 0 1

  3. tab页数据缓存,思考,公用div还是自用,跟交互和产品一起参与其中。参考淘宝,京东购物车交互,删除一项是否请求数据

删除成功时,物理去除该项,不要立刻刷新,如果再详情页改变状态,列表页手动赋值。 一页全部删除完之后 需要重新请求接口获取最新的 不能让页面空白 keepAlive缓存 每一项有单独的div记录自己的列表,不共用,为了防止用户滑到最下面,再次切换tab再切换回来数据不存在。

  1. 一件代发二期,落地页是无登录态也可

  2. 点击地址簿由于要获取用户地址信息,所以需要登录态,怎么获取?

    1. 如果使用window.location.href方式(默认)的话浏览器的栈顺序为免登陆下单页-1地址簿页面-登录页(成功)-2地址簿页面此时用户看到的地址簿页面是顶层的(2),当点击地址的某一条时 原来逻辑是回到免登陆下单页,但是事实却是回到底层的地址簿页面(1),会使地址簿展示两遍,给用户造成地址簿页面刷新的错觉。
    2. 如果使用window.location.replace方式的话浏览器的栈顺序为免登陆下单页-1地址簿页面-登录页《关掉地址簿页面》(成功)-2地址簿页面跳转到登录页的话 地址簿页面1会杀掉,返回到页面时会强制刷新免登陆下单页
    3. 最后采取的是 点击地址簿页面的时候,先调取下接口判断jdl下是否有登录态,有的话就不跳转登录页面,没有的话跳转登录页,下次回来就还是本页面。 但是问题来了 怎么判断jdl下有登录态,之前的接口是判断jd 域名下的cookie是否存在pt_key(为何要调取接口 因为该字段是httpOnly)
  3. 涉及到跨域如何解决?

    • 一开始的是jd.com 后来更改域名为jdl.cn 一开始不知道islogin接口是只判断jd下的登录态是否存在,就在jdl域名下调取了,导致跨域,最后使用jsonp解决,但是拿到的有时是false有时是true
    1. 为何登录了还是返回 false? 因为当浏览器只打开jdl域名时 跳转登录之后 下发的cookie的domin是jdl.cn 所以获取的一直是fasle
    2. 为何有时是true呢? 因为浏览器即打开了jd域下的其他项目又打开了jdl的项目,导致下发了cookie的domin有jdl的有jd的

    最后解决还是调用的某个接口 看是否返回401 判断是否有登录态

3.物参系统

因为项目后端出参一致,封装fetch,mockjs mock数据,降低联调成本

3.京东快递小程序

内嵌h5 打通登录态 往主站写入cookie 最终主站打通了登录态

因为客户端没有打通登录,业务又着急上线,所以前端拿到小程序的pin token,往主站的cookie里面写上对应的key value

3.小程序 多端融合

遇到很多的兼容问题

  • datset 不要使用大写,百度小程序平台不支持大写;
  • 组件渲染条件取 length 属性页面不更新,写个属性监听;
  • 对于微信原生代码中this.data.var=xx,在taro中不起作用,需要显示用this.data.setData({ var: xx })解决
  • Taro 路由会自动解码,跳转时需要进行编码(encodeURIComponent)操作:url?to={url}?to={encodeURIComponent(h5_url)}&tokenkey=${tokenkey}`
  • 模块导入:导入模块需要使用ES6 的 import , 不要使用 require ,图片和插件导入除外;
  • Taro 中 data 的属性和方法名称不能一样,内置了 computed 函数,导致报错,应该在属性上加一个动词来区分属性和方法; textareaFocus 和 this.handleTextareaFocus
  • 设置状态页面不刷新问题,需要采用数据深拷贝
  • 转译后代码有重定义变量导致页面栈溢出;分包加载的时候会把所有的分包的变量提升,spell页面里在data和onload中一个函数的初始化的变量提升冲突了(Taro的withWeapp做的提升),造成死循环注入,导致stack溢出
  • 小程序白屏,是因为在render的时候使用全局的变量渲染;解决方案,提取变量;原因:因为app是一个全局变量,每次渲染都会绑定,来回点击几次就会白屏;

image.png

公众号

  • 公众号静默授权

  • 公众号域名白名单