webpack问题的一些总结

按需加载

webpack 把 import() 作为一个分离点(split-point),并把引入的模块作为一个单独的 chunk。 import() 将模块名字作为参数并返回一个 Promoise 对象,即 import(name) -> Promise.

function determineDate() {
  import('moment').then(function(moment) {
    console.log(moment().format());
  }).catch(function(err) {
    console.log('Failed to load moment', err);
  });
}

提高构建速度


把 loader 应用的文件范围缩小,只在最少数必须的代码模块中去使用必要的 loader
使用happypack开启多线程

// @file: webpack.config.js
const HappyPack = require('happypack');

exports.module = {
  rules: [
    {
      test: /.js$/,
      // 1) replace your original list of loaders with "happypack/loader":
      // loaders: [ 'babel-loader?presets[]=es2015' ],
      use: 'happypack/loader',
      include: [ /* ... */ ],
      exclude: [ /* ... */ ]
    }
  ]
};

exports.plugins = [
  // 2) create the plugin:
  new HappyPack({
    // 3) re-add the loaders you replaced above in #1:
    loaders: [ 'babel-loader?presets[]=es2015' ]
  })
];

webpack打包分析

使用 webpack-bundle-analyzer

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
 
module.exports = {
  plugins: [
    new BundleAnalyzerPlugin()
  ]
}

抽取公共模块SplitChunksPlugin

以下是SplitChunksPlugin的默认配置:

splitChunks: {
    chunks: "async",
    minSize: 30000, // 模块的最小体积
    minChunks: 1, // 模块的最小被引用次数
    maxAsyncRequests: 5, // 按需加载的最大并行请求数
    maxInitialRequests: 3, // 一个入口最大并行请求数
    automaticNameDelimiter: '~', // 文件名的连接符
    name: true,
    cacheGroups: { // 缓存组
        vendors: {
            test: /[\\/]node_modules[\\/]/,
            priority: -10
        },
        default: {
            minChunks: 2,
            priority: -20,
            reuseExistingChunk: true
        }
    }
}

缓存组

缓存组因该是SplitChunksPlugin中最有趣的功能了。在默认设置中,会将 node_mudules 文件夹中的模块打包进一个叫 vendors的bundle中,所有引用超过两次的模块分配到 default bundle 中。更可以通过 priority 来设置优先级。

chunks

chunks属性用来选择分割哪些代码块,可选值有:’all’(所有代码块),’async’(按需加载的代码块),’initial’(初始化代码块)。

两天被问四次,可是我不会,关于前端设计模式

最近面了几家,有一个问题出现的频率过于高了。刚好准备时候没有覆盖到这部分,于是只能和面试官尬聊。 这个问题就是,你平时开发时候用到哪些开发模式。我只能凭着记忆说说单例模式,发布订阅模式啥的,驴头不对马嘴。至于面试结果,想必是不尽人意了。
那么,前端到底有哪些设计模式平时会用到呢。

单例模式

保证一个类只有一个实例,并提供一个全局访问点。
实现方法:先判断实例是否存在,不存在先创建后返回,存在则直接返回。
实际应用: 模态框,以及弹框,vuex中的store,mobx里的store。

let CreateDiv = function(html) {
        this.html = html;
};
let Singleton = (function() {
    let instance;
    return function(html) {
        if (!instance) {
            instance = new CreateDiv(html);
        }
        return instance;
    }
})();
let a = new Singleton('seven1');
let b = new Singleton('seven2');
console.log(a);
console.log(b);
console.log(a === b); // truej

工厂模式

工厂模式定义一个用于创建对象的接口,这个接口由子类决定实例化哪一个类。该模式使一个类的实例化延迟到了子类。而子类可以重写接口方法以便创建的时候指定自己的对象类型。

class Person {
    constructor(name) {
        this.name = name
    }
    getName() {
        console.log(this.name)
    }
}
class Factory {
    static create(name) {
        return new Person(name)
    }
}
Factory.create('caicai').getName()  //caicai

适配器模式

适配器模式就相当于一个转换接口,大家想想我们手机充电器通常是二岔口的,但是电源只有三岔口的。这时候就需要一个适配器把三岔口的转换成二岔口的。
它的作用其实就是解决两个软件实体间的接口不兼容问题,使用之后就可以一起工作了。

var googleMap = {
    show: function () {
        console.log('googleMap show!');
    }
}
var baiduMap = {
    display: function () {
        console.log('baiduMap show!');
    }
}

var renderMap = function (map) {
    if (map.show instanceof Function) {
        map.show()
    }
}

// 让baiduMap可以调用show方法
var baiduMapAdapter = {
    show:function(){
        return baiduMap.display()
    }
}
renderMap(googleMap);
renderMap(baiduMapAdapter);

代理模式

我们在事件代理的时候其实就是使用了代理模式,通过把监听事件全部交由父节点进行监听,这样你添加节点或者删除节点的时候就不用去改变监听的代码。
实际应用: 跨域、 事件代理

<ul id="ul">
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
</ul>
<script>
    let ul = document.querySelector('#ul')
    ul.addEventListener('click', (event) => {
        console.log(event.target);
    })
</script>
// ES6中的Proxy对象
const target = {}
const handler = {
    get(target, property) {
        if (property in target) {
            return target[property]
        } else {
            throw new ReferenceError("Property \"" + property + "\" does not exist.")
        }
    }
}
const p = new Proxy(target, {})
p.a = 3  // 被转发到代理的操作
console.log(p.c) //

发布-订阅模式

定义了一种依赖关系, 解决了主体对象与观察者之间功能的耦合
实际应用:vue的中央事件总线

// 这里想到一个前几天遇到的面试题,使用 ECMAScript(JS)代码实现一个事件类Event ,包含下面功能:绑定事件、解绑事件和派发事件。这里可以使用发布订阅模式
class Event {
  constructor () {
    // 储存事件的数据结构
    // 为查找迅速, 使用对象(字典)
    this._cache = {}
  }
 
  // 绑定
  on(type, callback) {
    // 为了按类查找方便和节省空间
    // 将同一类型事件放到一个数组中
    // 这里的数组是队列, 遵循先进先出
    // 即新绑定的事件先触发
    let fns = (this._cache[type] = this._cache[type] || [])
    if(fns.indexOf(callback) === -1) {
      fns.push(callback)
    }
    return this
  }
 
  // 触发
  trigger(type, data) {
    let fns = this._cache[type]
    if(Array.isArray(fns)) {
      fns.forEach((fn) => {
        fn(data)
      })
    }
    return this
  }
  
  // 解绑
  off (type, callback) {
    let fns = this._cache[type]
    if(Array.isArray(fns)) {
      if(callback) {
        let index = fns.indexOf(callback)
        if(index !== -1) {
          fns.splice(index, 1)
        }
      } else {
        // 全部清空
        fns.length = 0
      }
    }
    return this
  }
}

策略模式

根据情况进行不一样的方案,比如你想去旅游,明确自己有多少钱然后选择旅游方式。

  • 没钱,走路
  • 有钱,飞机
  • 还行,火车
var strategies = {
    "rich": function () {
        console.log("You can go with plane!");
    },
    "poor": function () {
        console.log("OH, You can go with your feet!");
    },
    "middle": function () {
        console.log("You can go with train!");
    }
}
var howShouldGo = function (money) {
    return strategies[money]();
}
console.log(howShouldGo("rich"));

迭代器模式

迭代器模式是指提供一种按顺序访问的方法。比如说我们经常使用的forEach方法,就是通过顺序访问的模式。我们可以自己去写一下forEach的方法。

var myForEach = function (arr, callback) {
    for (var i = 0, l = arr.length; i < l; i++) {
        callback.call(arr[i], i, arr[i]) //把元素以及下标传递出去
    }
}f

myForEach([1, 2, 3], function (item, n) {
    console.log([item, n]);
})
//[ 0, 1 ]
//[ 1, 2 ]
//[ 2, 3 ]

从一道面试题说起—你知道哪些前端性能优化方法

被裁员之后,在家休息了一周,现在开始苦兮兮的面试日子。面了几家,发现这个问题出现的频率过于高了,这里通过另一道面试题,浏览器输入URL后发生了什么为思路,做一个简单的反思总结。

浏览器输入URL并按下回车

无优化点

浏览器查找当前URL是否存在缓存

有一次面试,面试官问了个问题,你知道浏览器有哪些缓存方法。我当时第一反应是localstorage那一套东西,突突突报了一遍。面试官笑笑说,你这才说了一半,然后我人就傻了。回来后查了一些资料,原来想说的是这个东西。学习了一下相关的知识,这里记录一下。

强缓存(不走服务器)

强制缓存流程

在没有缓存数据的时候,浏览器向服务器请求数据时,服务器会将数据和缓存规则一并返回,缓存规则信息包含在响应header中。对于强制缓存来说,响应header中会有两个字段来标明失效规则(Expires/Cache-Control)
Expires值为服务器返回的到期时间,HTTP1.0标准,现已废弃
Cache-Control 有更详细的缓存规则

协商缓存(走服务器)

对比缓存流程

浏览器第一次请求数据时,服务器会将缓存标识与数据一起返回给客户端,客户端将二者备份至缓存数据库中。
再次请求数据时,客户端将备份的缓存标识发送给服务器,服务器根据缓存标识进行判断,判断成功后,返回304状态码,通知客户端比较成功,可以使用缓存数据。

Last-Modified 服务器在响应请求时,告诉浏览器资源的最后修改时间。
If-Modified-Since 再次请求服务器时,通过此字段通知服务器上次请求时,服务器返回的资源最后修改时间。

Etag  /  If-None-Match(优先级高于Last-Modified  /  If-Modified-Since)
Etag 服务器响应请求时,告诉浏览器当前资源在服务器的唯一标识(生成规则由服务器决定)。
If-None-Match 再次请求服务器时,通过此字段通知服务器客户段缓存数据的唯一标识。

总结

对于强制缓存,服务器通知浏览器一个缓存时间,在缓存时间内,下次请求,直接用缓存,不在时间内,执行比较缓存策略。
对于比较缓存,将缓存信息中的Etag和Last-Modified通过请求发送给服务器,由服务器校验,返回304状态码时,浏览器直接使用缓存。

优化点

服务器对静态资源设置浏览器缓存信息,浏览器在有缓存的情况下直接从本地读取资源。

DNS域名解析

当浏览器访问一个域名的时候,需要解析一次DNS,获得对应域名的ip地址。在解析过程中,按照浏览器缓存系统缓存路由器缓存ISP(运营商)DNS缓存根域名服务器顶级域名服务器主域名服务器的顺序,逐步读取缓存,直到拿到IP地址
DNS Prefetch,即DNS预解析就是根据浏览器定义的规则,提前解析之后可能会用到的域名,使解析结果缓存到系统缓存中,缩短DNS解析时间,来提高网站的访问速度

<link rel="dns-prefetch" href="https://www.google.com">
<link rel="dns-prefetch" href="https://www.google-analytics.com">

CDN

使用cdn分发,将用户的请求重新导向距离较近的服务节点。其目的是使用户可就近取得所需内容,解决 Internet网络拥挤的状况,提高用户访问网站的响应速度

TCP连接

DNS解析后得到了服务器的ip,接下来就是和服务器建立起连接,这通过TCP的三次握手完成。

浏览器向服务器发送HTTP请求

减少请求次数,为什么要减少请求次数,
合并外部请求的js、css文件
运用CSS精灵合并处理多个icon文件、运用图标字体、把小图标转为base64

浏览器接收响应体

对请求的文件进行打包(webpack),减少文件体积,使用gzip压缩

页面渲染

浏览器构建的步骤,大概如下:
1、处理HTML标记并构建DOM树
2、处理CSS标记并构建CSSOM树
3、将DOM与CSSOM合并成一个渲染树
4、根据渲染树来布局,以计算每个节点的集合信息
5、将各个节点绘制到屏幕

js的影响

js是会阻塞页面渲染,所以把js放在body底部,或者异步

css的影响

前端页面渲染时会根据DOM结构生成一个DOM树,然后加上CSS样式生成渲染树。如果CSS文件放在标签中,则CSS Rule Tree会先于DOM树完成构建,之后浏览器就可以边构建DOM树边完成渲染;反之,CSS文件放在所有页面标签之后,比如之前,那么当DOM树构建完成了,渲染树才构建,浏览器不得不再重新渲染整个页面,这样造成了资源的浪费。而且页面还可能会出现闪跳的感觉,或者白屏或者布局混乱或者样式很丑,直到CSS加载完成,页面重绘才能恢复正常。
因此,一般来讲,css标签应放在标签之间。但如果css文件较大,会让首页白屏时间更长,所以并不是说把css都放顶部是一个完美的方法。权衡利弊,应该把必须的css(js)放顶部,把不那么重要的css(js)放底部。

千辛万苦,终于做出来了!

自从会写html开始就一直想搞一个自己的博客,开始碍于能力有限,一直没找到头绪。时隔一年,终于做好了,记录一下小踩的坑。

后台搭建

命令行弱鸡果断选择现成的面板工具。使用宝塔进行全局的运维管理,方便快捷, 各种环境一键搭建。这真不是广告,实在是太友好了。

现有的环境选择nginx+mysql,以后打算增加node的管理接口,先摆个坑在这。ftp、phpmyadmin这些都是宝塔一键安装,当个添头,操作起来也很方便。node进程现在用pm2管理,回头还要补下文档,暂时只跑了一个wakatime-async,可以把wakatiame的数据存到gist上,定时任务每天凌晨一点半抓一下,通过server酱推送到微信。

其他应用全跑docker上,体感上宿主机各种进程管理会比较清晰。

Docker踩坑

基于之前已经操作过docker,对其命令还是略有了解,这里记录一下奇奇怪怪的坑。

博客先选用了wordpress,docker一键搭建。后台苦手,自己搭一套文章管理怕不是再过两年,先跑起来再说。

因为宿主机上的宝塔已经建了sql服务,这里选择wordpressdocker连接宿主机sql服务。于是就有了第一个大坑,容器跑起来

docker run -d --name wordpress -e WORDPRESS_DB_HOST=localhost:3306 -p 1080:80 wordpress

进1080端口,咦,发现连不上服务器。

原来这里WORDPRESS_DB_HOSTlocalhost就会到容器自己的localhost,连不到宿主机上。这里搜了些资料,宿主机ifconfig命令找到docker的虚拟网卡ip,填上,还是连不上服务器??这里就是第二个大坑了,左思右想,寻思可能是数据库权限的问题,因为新装的数据库不允许外部访问,于是

 update user set host=‘%’ where user=‘root’

好了,这回进1080端口,一切正常了。

nginx

再然后,现在还需要加端口号,我想直接输入域名就可以访问我的小破站。配了一下nginx

server 
{
    listen 80;
    server_name luv.plus;
    index index.html index.htm index.php default.html default.htm default.php;
    location /
    {
        proxy_pass   http://127.0.0.1:1080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-PORT $remote_port;
        proxy_set_header X-Real-IP $remote_addr; 
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

中间有一个坑是没有加set_header的部分,一进网站就跳127.0.0.1。没弄明白怎么回事,回头再补nginx的相关知识吧。