Druidiurd

Druidiurd 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

Druidiurd 提出了问题 · 2020-04-19

volatile 保证可见性疑惑?

volatile关键字保证数据在各个CPU高速缓存中的可见性.

一个CPU改变了其缓存的值, 会立即更新主内存的值, 并使得其他CPU中的缓存失效.

这貌似是一句简单而正确的话, 我不理解其中的细节. 这句话实际上干了两件事, 一个刷新内存, 一个通知其他CPU缓存失效

我想问一下, 这个两个操作是原子操作吗?

其他CPU发现缓存失效之后, 必须从主内存中重新获取值, 那么其内部不依赖这个缓存数据的指令能否并行执行, 或者说必须先lock, 取回新值之后才能重新执行计算?

关注 2 回答 1

Druidiurd 收藏了文章 · 2018-06-25

JS忍者秘籍中的定时器机制详解

前言

前段时间刚看完《JS忍者秘籍》,虽说是15年出版的,有些东西是过时了,但像对原型链、闭包、正则、定时器之类的机制却是不会过时的,里面很多东西都讲的很细,还是值得一读的,本文将对这本书中对定时器机制的部分进行详细的解析,如果喜欢的话可以点波赞/关注,支持一下,希望大家看完本文可以有所收获。

个人博客了解一下:obkoro1.com

准备

阅读本文之前,推荐先阅读Js 的事件循环(Event Loop)机制以及实例讲解这篇文章来理解背后发生的事情,本文对事件循环机制不会很仔细的讲解。

定时器解决的问题:

由于JS的单线程特性,定时器提供了一种跳出单线程限制的方法,即让一段代码在一定毫秒之后,再异步执行。

设置和清除定时器:

直接引用忍者秘籍中的图片:

注意:

  1. 定时器的时间间隔设为0,也会有几毫秒的延迟。
  2. 在使用setTimeoutsetInterval的时候最好将其赋值给一个变量,以便取消定时器。
  3. 在使用Vue的时候,setTimeoutsetInterval的this指向的是window对象,访问不到组件数据以及方法。
  4. 在使用Vue的时候,路由跳转并不会销毁setInterval,但是组件已经销毁了,这会导致问题。
  5. 在执行线程中setTimeout/setInterval无法保证准时执行回调函数的。
  6. setInterval调用有可能会被废弃以及setInterval的连续执行

第三点和第四点的解决方法可以参考我之前写的Vue 实践过程中的几个问题

接下来要讲的是第五点和第六点,这两点是最重要,也是本文要重点解析的内容。

执行线程中的定时器执行

下面来看忍者秘籍中的栗子:

让我们看看这里发生了什么事情:

  1. 首先在0毫秒的时候有一个持续18毫秒的js代码块要执行。
  2. 然后在0毫秒的时候设了两个10毫秒延迟的定时器,setTimeout以及setInterval,setTimeout先设定。
  3. 在第6毫秒的时候有一个发生了鼠标单击事件。

事件排队。

同时发生了这么多事情,由于js的单线程特性,当线程正在执行状态,有异步事件触发时,它就会排队,并且在线程空闲时才进行执行

    这里的异步事件包括:鼠标单击,定时器触发,ajax请求、promise等事件。

让我们回到栗子中:

栗子中首先有一个18毫秒的代码块要执行,在这18毫秒中只能执行这段代码块,其他事件触发了之后只能排队等待执行

在代码块还在运行期间,第6毫秒的时候,发生了一个鼠标单击事件,以及第10毫秒时的setTimeoutsetInterval两个处理程序,这三个事件不能立即执行,而是被添加到等待执行的队列中。

先进先出(先排队的先执行)

18毫秒的时候代码块结束执行,有三个任务在排队等待执行,根据先进先出的原则,此时会先执行click事件setTimeoutsetInterval将继续排队等待执行。

setInterval调用被废弃

在click事件执行时,第20毫秒处,第二个setInterval也到期了,因为此时已经click事件占用了线程,所以setInterval还是不能被执行,并且因为此时队列中已经有一个setInterval正在排队等待执行,所以这一次的setInterval的调用将被废弃

浏览器不会对同一个setInterval处理程序多次添加到待执行队列。

setTimeout/setInterval无法保证准时执行回调函数

click事件在第28毫秒处结束执行,有两个任务(setTimeoutsetInterval)正在等待执行,遵循先进先出的原则,setTimeout早于setInterval设定,所以先执行setTimeout

so:我们期望在第10毫秒处执行的setTimeout处理程序,最终会在第28毫秒处才开始执行,这就是上文提到的setTimeout/setInterval无法保证准时执行回调函数。

在30毫秒处,setInterval又触发了,因为队列中已经有setInterval在排队,所以这次的触发又作废了。

setInterval的连续执行

setTimeout执行结束,在第36毫秒处,队列中的setInterval处理程序才开始执行,setInterval需要执行6毫秒。

在第40毫秒的时候setInterval再次触发,因为此时上一个setInterval正在执行期间,队列中并没有setInterval在排队,这次触发的setInterval将进入队列等候

所以:setInterval的处理时长不能比设定的间隔长,否则setInterval将会没有间隔的重复执行

第42毫秒的时候,第一个setInterval结束,然后队列中的setInterval立即开始执行,在48毫秒的时候完成执行。然后50毫秒的时候再次触发setInterval,此时没有任务在排队,将会立即执行。

setTimeout按照一定的间隔周期性的触发定时器。

上文说了,setInterval的处理时长不能比设定的间隔长,否则setInterval将会没有间隔的重复执行

但是对这个问题,很多情况下,我们并不能清晰的把控处理程序所消耗的时长,为了我们能按照一定的间隔周期性的触发定时器,忍者秘籍中提供了下面这种使用方法:

    // 实际上我不止在忍者秘籍中见过,在很多地方都见过这种技术。
    setTimeout(function repeatMe(){
      // do something
      setTimeout(repeatMe,10); 
      // 执行完处理程序的内容后,在末尾再间隔10毫秒来调用该程序,这样就能保证一定是10毫秒的周期调用
    },10)

忍者秘籍中关于定时器的其他知识:

  • 定时器不能非常细粒化的控制执行的时间,书中建议在15ms以上。
  • 可以使用定时器来分解长时间运行的任务,这里可以自行谷歌。

任务队列只有排队这么简单吗?

事实上,关于任务队列并不是只有简单的排队而已,忍者秘籍中提到为了方便,使用了这个概念,如果希望能更清晰的了解背后的机制,再次推荐阅读一下:Js 的事件循环(Event Loop)机制以及实例讲解


结语

这上面所有一切,都是由js单线程特性导致的,所以会有事件排队、先进先出、setInterval调用被废弃、定时器无法保证准时执行回调函数以及出现setInterval的连续执行,时刻记住这一特性,很多关于事件执行顺序的问题都能想的通,并且找出解决方法。

希望看完的朋友可以点个喜欢/关注,您的支持是对我最大的鼓励。

个人blog and 掘金个人主页,如需转载,请放上原文链接并署名。码字不易,感谢支持!本人写文章本着交流记录的心态,写的不好之处,不撕逼,但是欢迎指点。

如果喜欢本文的话,欢迎关注我的订阅号,漫漫技术路,期待未来共同学习成长。

以上2018.6.17

参考资料:

JS忍者秘籍第8章:驯服线程和定时器

查看原文

Druidiurd 收藏了文章 · 2018-06-25

深入理解css之vertical-align

前言

vertical-align用来指定行内元素(inline)或表格单元格(table-cell)元素的垂直对齐方式。也就是说,对于块级元素,vertical-align是不起作用的。

vertical-align的各类属性值

vertical-align的属性值可以归为以下4类:

  • 线类,如 baseline、top、middle、bottom;
  • 文本类,如 text-top、text-bottom;
  • 上标下标类,如 sub、super;
  • 数值百分比类,如 10px、1em、5%;

线类

baseline,baseline为vertical-align的默认值,其意思是指基线对齐,所谓基线,指的是字母x的下边缘,具体可看前文深入理解css之line-height有讲解到,不懂的小伙伴建议先看看这篇文章。我们来看个例子,代码如下:


.box {
    width: 100px;
    line-height: 100px;
    border: 1px solid #ccc;
}

<div class="box">
    <span class="text">文本</span>
</div>

效果如下:

图片描述

由于baseline是默认值,所以可以不用写。.box的line-height为100px,这其实是给“strut”设置的(不懂strut概念的可以看看前面的文章深入理解css盒子模型,简单点说就是每一个行框盒子都有一个看不见的节点,该节点继承了line-height),因此.text对齐于该节点的基线(可以想象成这个看不见的节点有一个字母x,而.text就是跟这个字母x的下边缘对齐)

关于baseline,有一个需要注意的地方就是inline-block元素,如果一个inline-block元素,里面没有内联元素,或者overflow不是visible,则该元素的基线是其margin底边缘;否则其基线就是元素里面最后一行内联元素的基线。例子如下:

.text {
    display: inline-block;
    width: 100px;
    height: 100px;
    border: 1px solid #ccc;
}
<div class="container">
    <span class="text">文本</span>
    <span class="text"></span>
</div>

效果如下:

图片描述

top,对于内联元素,指的是元素的顶部和当前行框盒子的顶部对齐;对于table-cell元素,指的是元素的顶padding边缘和表格行的顶部对齐。例子如下:

.box {
    width: 100px;
    line-height: 100px;
    border: 1px solid #ccc;
}
.top {
    line-height: normal;
    vertical-align: top;
}

<div class="box">
    <span class="top">文本</span>
</div>

效果如下:

图片描述

bottom,跟top类似,将顶部换成底部即可。

middle,这个属性值用得比较多。对于内联元素指的是元素的垂直中心点与行框盒子基线往上1/2x-height处对齐,简单点说就是字母x的中心位置对齐;对于table-cell元素,指的是单元格填充盒子相对于外面的表格行居中对齐。基本上所有字体中,字母x的位置都是偏下一点的,font-size越大偏移越明显,因此,字母x中心的位置不是行框盒子的中心,也就是说vertical-align只能实现近似垂直居中对齐。

文本类

text-top,指的是盒子的顶部和父级内容区域的顶部对齐。

text-bottom,指的是盒子的底部和父级内容区域的底部对齐。

例子如下:

.box {
    width: 300px;
    line-height: 100px;
    border: 1px solid #ccc;
    font-size: 20px;
}
.f12 {
    font-size: 12px;
}
.f16 {
    font-size: 16px;
}
.f20 {
    font-size: 20px;
}
.text-top {
    line-height: normal;
    vertical-align: text-top;
    width: 100px;
}
<div class="box">
    <span class="f12">12px</span>
    <span class="f16">16px</span>
    <span class="f20">20px</span>
    <img class="text-top" data-original="./card.jpg"/>
</div>

效果如下:

图片描述

所谓内容区域,可以看成是鼠标选中文字后高亮的背景色区域,上面的例子中,由于父元素设置的是20px,所以图片的vertical-align设置text-top的时候,就可以看成是跟子元素为20px元素的内容区域顶部对齐。

上标下标类

上标和下标对应着两个标签super和sub,super在上面,sub在下面,这两个属性值在数学公式和化学表达式中用得比较多,平时我们开发几乎用不到,也没啥好讲的。

数值百分比类

vertical-align是支持数值的,并且兼容性也非常好,但大部分开发人员却不知道vertical-align支持数值。对于数值,正值表示由基线往上偏移,负值表示由基线往下偏移。而百分比则是基于line-height来计算的,百分比用得比较少,因为line-height一般都是开发人员给出的,这时候数值就可以精确定位元素,不需要再使用百分比再去计算一遍。使用数值的代码如下:

.box {
    width: 300px;
    line-height: 100px;
    border: 1px solid #ccc;
    font-size: 20px;
}
.num {
    line-height: normal;
    vertical-align: 20px;
}

<div class="box">
    <span class="num">文本</span>
</div>

效果如下:

图片描述

vertical-align起作用的前提

vertical-align起作用是有前提条件的,这个前提条件就是:只能应用于内联元素以及display值为table-cell的元素。在css中,有些css属性是会改变元素的display值的,例如float和position: absolute,一旦设置了这两个属性之一,元素的display值就是变为block,因此,vertical-align也就失去了作用。下面这段代码这样写就是错的:


   span {
        float: left;
        vertical-align: middle; /* 错误,该行代码无效 */
   }

另外,更多人遇到的是以下这种无效的情况:


.box {
    height: 200px;
}
.box > img {
    height: 100px;
    vertical-align: middle;
}

<div class="box">
    <img  data-original="1.jpg" />
</div>

其实,不是vertical-align无效,而是前面所说的“strut”的影响,由于.box没有设置line-height,所以“strut”的line-height就非常小,比图片的高度小很多,vertical-align: middle没法发挥作用。这时给.box一个比较高的line-height,就会看到vertical-align起作用了:


.box {
    height: 200px;
    line-height: 200px;
}

vertical-align与line-height的关系

前面讲了,vertical-align的百分比值是根据line-height来计算的。但实质上,只要是内联元素,这两个元素都会同时在起作用。如下例子:

.box {
    line-height: 32px;
}
.box > span {
    font-size: 24px;
}

<div class="box">
    <span>文本</span>
</div>

效果如下:

图片描述

从代码上看,好像.box的高度会是32px,但实质上.box的高度会比32px还要高。原因是"strut"继承了line-height: 32px,span也继承了line-height: 32px,但两者的font-size不一样,这就导致了"strut"的font-size比较小,而span的font-size比较大,也就是说它们的基线不在同一位置上,"strut"偏上一点,而span默认又是基线对齐,为此,span总体会往上移以便跟"strut"基线对齐,.box元素就是这样被撑高了。而解决方案可以有以下几种:

  • span元素不使用基线对齐,可以改为top对齐
  • span元素块状化
  • line-height设置为0
  • font-size设置为0

总结

  • 讲解了vertical-align的各类属性值及其效果
  • vertical-align起作用的前提是内联元素
  • vertical-align与line-height都是同时作用在内联元素上的
查看原文

Druidiurd 收藏了文章 · 2018-06-14

详解CommonsChunkPlugin的配置和用法

简介

CommonsChunkPlugin主要是用来提取第三方库和公共模块,避免首屏加载的bundle文件或者按需加载的bundle文件体积过大,从而导致加载时间过长,着实是优化的一把利器。

先来说一下各种教程以及文档中CommonsChunkPlugin提及到chunk有哪几种,主要有以下三种:

  1. webpack当中配置的入口文件(entry)是chunk,可以理解为entry chunk
  2. 入口文件以及它的依赖文件通过code split(代码分割)出来的也是chunk,可以理解为children chunk
  3. 通过CommonsChunkPlugin创建出来的文件也是chunk,可以理解为commons chunk

CommonsChunkPlugin可配置的属性

  • name:可以是已经存在的chunk(一般指入口文件)对应的name,那么就会把公共模块代码合并到这个chunk上;否则,会创建名字为name的commons chunk进行合并
  • filename:指定commons chunk的文件名
  • chunks:指定source chunk,即指定从哪些chunk当中去找公共模块,省略该选项的时候,默认就是entry chunks
  • minChunks:既可以是数字,也可以是函数,还可以是Infinity,具体用法和区别下面会说

children和async属于异步中的应用,放在了最后讲解。

可能这么说,大家会云里雾里,下面用demo来检验上面的属性。

实战应用

以下几个demo主要是测试以下几种情况:

  • 不分离出第三方库和自定义公共模块
  • 分离出第三方库、自定义公共模块、webpack运行文件,但它们在同一个文件中
  • 单独分离第三方库、自定义公共模块、webpack运行文件,各自在不同文件

不分离出第三方库和自定义公共模块

项目初始结构,后面打包后会生成dist目录:

image

src目录下各个文件内容都很简洁的,如下:

common.js
export const common = 'common file';

first.js
import {common} from './common';
import $ from 'jquery';
console.log($,`first  ${common}`);

second.js
import {common} from './common';
import $ from 'jquery';
console.log($,`second ${common}`);

package.json文件:

{
  "name": "test",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "rimraf dist && webpack"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "rimraf": "^2.6.2",
    "webpack": "^3.10.0",
    "webpack-dev-server": "^2.10.1"
  },
  "dependencies": {
    "jquery": "^3.2.1"
  }
}

webpack.config.js:

const path = require("path");
const webpack = require("webpack");

const config = {
    entry: {
        first: './src/first.js',
        second: './src/second.js'
    },
    output: {
        path: path.resolve(__dirname,'./dist'),
        filename: '[name].js'
    },
}

module.exports = config;

接着在命令行npm run build,此时项目中多了dist目录:

image

再来查看一下命令行中webpack的打包信息:

image

查看first.js和second.js,会发现共同引用的common.js文件和jquery都被打包进去了,这肯定不合理,公共模块重复打包,体积过大。

分离出第三方库、自定义公共模块、webpack运行文件

这时候修改webpack.config.js新增一个入口文件vendor和CommonsChunkPlugin插件进行公共模块的提取:

const path = require("path");
const webpack = require("webpack");
const packagejson = require("./package.json");

const config = {
    entry: {
        first: './src/first.js',
        second: './src/second.js',
        vendor: Object.keys(packagejson.dependencies)//获取生产环境依赖的库
    },
    output: {
        path: path.resolve(__dirname,'./dist'),
        filename: '[name].js'
    },
    plugins: [
        new webpack.optimize.CommonsChunkPlugin({
            name: 'vendor',
            filename: '[name].js'
        }),
    ]
}

module.exports = config;

查看dist目录下,新增了一个vendor.js的文件:

image

再来查看一下命令行中webpack的打包信息:

image

通过查看vendor.js文件,发现first.js和second.js文件中依赖的jquery和common.js都被打包进vendor.js中,同时还有webpack的运行文件。总的来说,我们初步的目的达到,提取公共模块,但是它们都在同一个文件中。

到这里,肯定有人希望自家的vendor.js纯白无瑕,只包含第三方库,不包含自定义的公共模块和webpack运行文件,又或者希望包含第三方库和公共模块,不包含webpack运行文件。

其实,这种想法是对,特别是分离出webpack运行文件,因为每次打包webpack运行文件都会变,如果你不分离出webpack运行文件,每次打包生成vendor.js对应的哈希值都会变化,导致vendor.js改变,但实际上你的第三方库其实是没有变,然而浏览器会认为你原来缓存的vendor.js就失效,要重新去服务器中获取,其实只是webpack运行文件变化而已,就要人家重新加载,好冤啊~

OK,接下来就针对这种情况来测试。

单独分离出第三方库、自定义公共模块、webpack运行文件

这里我们分两步走:

  1. 先单独抽离出webpack运行文件
  2. 接着单独抽离第三方库和自定义公共模块,这里利用minChunks有两种方法可以完成,往后看就知道了

1、抽离webpack运行文件

先来抽离webpack运行文件,修改webpack配置文件:

plugins: [
        new webpack.optimize.CommonsChunkPlugin({
            name: ['vendor','runtime'],
            filename: '[name].js'
        }),
    ]

其实上面这段代码,等价于下面这段:

 plugins: [
        new webpack.optimize.CommonsChunkPlugin({
            name: 'vendor',
            filename: '[name].js'
        }),
        new webpack.optimize.CommonsChunkPlugin({
            name: 'runtime',
            filename: '[name].js',
            chunks: ['vendor']
        }),
    ]

上面两段抽离webpack运行文件代码的意思是创建一个名为runtime的commons chunk进行webpack运行文件的抽离,其中source chunks是vendor.js。

查看dist目录下,新增了一个runtime.js的文件,其实就是webpack的运行文件:

image

再来查看一下命令行中webpack的打包信息,你会发现vendor.js的体积已经减小,说明已经把webpack运行文件提取出来了:

image

可是,vendor.js中还有自定义的公共模块common.js,人家只想vendor.js拥有项目依赖的第三方库而已(这里是jquery),这个时候把minChunks这个属性引进来。

minChunks可以设置为数字、函数和Infinity,默认值是2,并不是官方文档说的入口文件的数量,下面解释下minChunks含义:

  • 数字:模块被多少个chunk公共引用才被抽取出来成为commons chunk
  • 函数:接受 (module, count) 两个参数,返回一个布尔值,你可以在函数内进行你规定好的逻辑来决定某个模块是否提取成为commons chunk
  • Infinity:只有当入口文件(entry chunks) >= 3 才生效,用来在第三方库中分离自定义的公共模块

2、抽离第三方库和自定义公共模块

要在vendor.js中把第三方库单独抽离出来,上面也说到了有两种方法。

第一种方法minChunks设为Infinity,修改webpack配置文件如下:

plugins: [
        new webpack.optimize.CommonsChunkPlugin({
            name: ['vendor','runtime'],
            filename: '[name].js',
            minChunks: Infinity
        }),
        new webpack.optimize.CommonsChunkPlugin({
            name: 'common',
            filename: '[name].js',
            chunks: ['first','second']//从first.js和second.js中抽取commons chunk
        }),
    ]

查看dist目录下,新增了一个common.js的文件:

image

再来查看一下命令行中webpack的打包信息,自定义的公共模块分离出来:

image

这时候的vendor.js就纯白无瑕,只包含第三方库文件,common.js就是自定义的公共模块,runtime.js就是webpack的运行文件。

第二种方法把它们分离开来,就是利用minChunks作为函数的时候,说一下minChunks作为函数两个参数的含义:

  • module:当前chunk及其包含的模块
  • count:当前chunk及其包含的模块被引用的次数

minChunks作为函数会遍历每一个入口文件及其依赖的模块,返回一个布尔值,为true代表当前正在处理的文件(module.resource)合并到commons chunk中,为false则不合并。

继续修改我们的webpack配置文件,把vendor入口文件注释掉,用minChunks作为函数实现vendor只包含第三方库,达到和上面一样的效果:

const config = {
    entry: {
        first: './src/first.js',
        second: './src/second.js',
        //vendor: Object.keys(packagejson.dependencies)//获取生产环境依赖的库
    },
    output: {
        path: path.resolve(__dirname,'./dist'),
        filename: '[name].js'
    },
     plugins: [
        new webpack.optimize.CommonsChunkPlugin({
            name: 'vendor',
            filename: '[name].js',
            minChunks: function (module,count) {
                console.log(module.resource,`引用次数${count}`);
                //"有正在处理文件" + "这个文件是 .js 后缀" + "这个文件是在 node_modules 中"
                return (
                    module.resource &&
                    /\.js$/.test(module.resource) &&
                    module.resource.indexOf(path.join(__dirname, './node_modules')) === 0
                )
            }
        }),
        new webpack.optimize.CommonsChunkPlugin({
            name: 'runtime',
            filename: '[name].js',
            chunks: ['vendor']
        }),
    ]
}

上面的代码其实就是生成一个叫做vendor的commons chunk,那么有哪些模块会被加入到vendor中呢?就对入口文件及其依赖的模块进行遍历,如果该模块是js文件并且在node_modules中,就会加入到vendor当中,其实这也是一种让vendor只保留第三方库的办法。

再来查看一下命令行中webpack的打包信息:

image

你会发现,和上面minChunks设为Infinity的结果是一致的。

children和async属性

这两个属性主要是在code split(代码分割)和异步加载当中应用。

  • children

    • 指定为true的时候,就代表source chunks是通过entry chunks(入口文件)进行code split出来的children chunks
    • children和chunks不能同时设置,因为它们都是指定source chunks的
    • children 可以用来把 entry chunk 创建的 children chunks 的共用模块合并到自身,但这会导致初始加载时间较长
  • async:即解决children:true时合并到entry chunks自身时初始加载时间过长的问题。async设为true时,commons chunk 将不会合并到自身,而是使用一个新的异步的commons chunk。当这个children chunk 被下载时,自动并行下载该commons chunk

修改webpack配置文件,增加chunkFilename,如下:

 output: {
        ...........
        chunkFilename: "[name].[hash:5].chunk.js",
    },
plugins: [
    new webpack.optimize.CommonsChunkPlugin({
        name: ['vendor','runtime'],
        filename: '[name].js',
        minChunks: Infinity
    }),
   new webpack.optimize.CommonsChunkPlugin({
        children: true,
        async: 'children-async'
    })
]

chunkFilename用来指定异步加载的模块名字,异步加载模块中的共同引用到的模块就会被合并到async中指定名字,上面就是children-async。

修改成异步截图出来太麻烦了,就简单说明一下:first和second是异步加载模块,同时它们共同引用了common.js这个模块,如果你不设置这一步:

 new webpack.optimize.CommonsChunkPlugin({
        children: true,
        async: 'children-async'
    })

那么共同引用的common.js都被打包进各自的模块当中,就重复打包了。

OK,你设置之后,也得看children的脸色怎么来划分:

  • children为true,共同引用的模块就会被打包合并到名为children-async的公共模块,当你懒加载first或者second的时候并行加载这和children-async公共模块
  • children为false,共同引用的模块就会被打包到首屏加载的app.bundle当中,这就会导致首屏加载过长了,而且也不要用到,所以最好还是设为true

浏览器缓存的实现

先来说一下哈希值的不同:

  • hash 是 build-specific ,即每次编译都不同——适用于开发阶段
  • chunkhash 是 chunk-specific,是根据每个 chunk 的内容计算出的 hash——适用于生产

所以,在生产环境,要把文件名改成'[name].[chunkhash]',最大限度的利用浏览器缓存。

最后,写这篇文章,自己测试了很多demo,当然不可能全部贴上,但还是希望自己多动手测试以下,真的坑中带坑。

也参考了很多文章:

查看原文

Druidiurd 收藏了文章 · 2018-06-05

JS 模块化编程 require.js 的用法

什么是RequireJS?

RequireJS 是一个JavaScript模块加载器。它非常适合在浏览器中使用, 但它也可以用在其他脚本环境, 就像 Rhino and Node. 使用RequireJS加载模块化脚本将提高代码的加载速度和质量。

为什么用RequireJS?

  1. 异步“加载”。我们知道,通常网站都会把script脚本的放在html的最后,这样就可以避免浏览器执行js带来的页面阻塞。使用RequireJS,会在相关的js加载后执行回调函数,这个过程是异步的,所以它不会阻塞页面。

  2. 按需加载。通过RequireJS,你可以在需要加载js逻辑的时候再加载对应 的js模块,这样避免了在初始化网页的时候发生大量的请求和数据传输,或许对于一些人来说,某些模块可能他根本就不需要,那就显得没有必要。

  3. 更加方便的模块依赖管理。相信你曾经一定遇到过因为script标签顺序问题而导致依赖关系发生错误,这个函数未定义,那个变量undefine之类的。通过RequireJS的机制,你能确保在所有的依赖模块都加载以后再执行相关的文件,所以可以起到依赖管理的作用。

  4. 更加高效的版本管理。想一想,如果你还是用的script脚本引入的方式来引入一个jQuery2.x的文件,然后你有100个页面都是这么引用的,那当你想换成jQuery3.x,那你就不得不去改这100个页面。但是如果你的requireJS有在config中做jQuery的path映射,那你只需要改一处地方即可。

  5. 当然还有一些诸如cdn加载不到js文件,可以请求本地文件等其它的优点,这里就不一一列举了。

require.js的加载

使用require.js的第一步,是先去官方网站下载最新版本。
下载后,假定把它放在js子目录下面,就可以加载了。

<script data-original="js/require.js"></script>

加载require.js以后,下一步就要加载我们自己的代码了。假定我们自己的代码文件是main.js,也放在js目录下面。那么,只需要写成下面这样就行了:

<script data-original="js/require.js" data-main="js/main"></script>

data-main属性的作用是,指定网页程序的主模块。在上例中,就是js目录下面的main.js,这个文件会第一个被require.js加载。由于require.js默认的文件后缀名是js,所以可以把main.js简写成main。

主模块的写法

上一节的main.js,我把它称为"主模块",意思是整个网页的入口代码。它有点像C语言的main()函数,所有代码都从这儿开始运行。
下面就来看,怎么写main.js。

// main.js
require(['moduleA', 'moduleB', 'moduleC'], function (moduleA, moduleB, moduleC){
  // some code here
});

require()函数接受两个参数。第一个参数是一个数组,表示所依赖的模块,上例就是['moduleA', 'moduleB', 'moduleC'],即主模块依赖这三个模块;第二个参数是一个回调函数,当前面指定的模块都加载成功后,它将被调用。加载的模块会以参数形式传入该函数,从而在回调函数内部就可以使用这些模块。
require()异步加载moduleA,moduleB和moduleC,浏览器不会失去响应;它指定的回调函数,只有前面的模块都加载成功后,才会运行,解决了依赖性的问题。
下面,我们看一个实际的例子。
假定主模块依赖jquery、underscore和backbone这三个模块,main.js就可以这样写:

require(['jquery', 'underscore', 'backbone'], function ($, _, Backbone){
  // some code here
});

模块的加载

上一节最后的示例中,主模块的依赖模块是['jquery', 'underscore', 'backbone']。默认情况下,require.js假定这三个模块与main.js在同一个目录,文件名分别为jquery.js,underscore.js和backbone.js,然后自动加载。
使用require.config()方法,我们可以对模块的加载行为进行自定义。require.config()就写在主模块(main.js)的头部。参数就是一个对象,这个对象的paths属性指定各个模块的加载路径。

  require.config({
    paths: {
      "jquery": "jquery.min",
      "underscore": "underscore.min",
      "backbone": "backbone.min"
    }
  });

上面的代码给出了三个模块的文件名,路径默认与main.js在同一个目录(js子目录)。如果这些模块在其他目录,比如js/lib目录,则有两种写法。一种是逐一指定路径。

  require.config({
    paths: {
      "jquery": "lib/jquery.min",
      "underscore": "lib/underscore.min",
      "backbone": "lib/backbone.min"
    }
  });

另一种则是直接改变基目录(baseUrl)。

  require.config({
    baseUrl: "js/lib",
    paths: {
      "jquery": "jquery.min",
      "underscore": "underscore.min",
      "backbone": "backbone.min"
    }
  });

AMD模块的写法

require.js加载的模块,采用AMD规范。也就是说,模块必须按照AMD的规定来写。
具体来说,就是模块必须采用特定的define()函数来定义。如果一个模块不依赖其他模块,那么可以直接定义在define()函数之中。
假定现在有一个math.js文件,它定义了一个math模块。那么,math.js就要这样写:

  // math.js
  define(function (){
    var add = function (x,y){
      return x+y;
    };
    return {
      add: add
    };
  });

加载方法如下:

  // main.js
  require(['math'], function (math){
    alert(math.add(1,1));
  });

如果这个模块还依赖其他模块,那么define()函数的第一个参数,必须是一个数组,指明该模块的依赖性。

  define(['myLib'], function(myLib){
    function foo(){
      myLib.doSomething();
    }
    return {
      foo : foo
    };
  });

当require()函数加载上面这个模块的时候,就会先加载myLib.js文件

加载非规范的模块

理论上,require.js加载的模块,必须是按照AMD规范、用define()函数定义的模块。但是实际上,虽然已经有一部分流行的函数库(比如jQuery)符合AMD规范,更多的库并不符合。那么,require.js是否能够加载非规范的模块呢?
回答是可以的。
这样的模块在用require()加载之前,要先用require.config()方法,定义它们的一些特征。
举例来说,underscore和backbone这两个库,都没有采用AMD规范编写。如果要加载它们的话,必须先定义它们的特征。

  require.config({
    shim: {

      'underscore':{
        exports: '_'
      },
      'backbone': {
        deps: ['underscore', 'jquery'],
        exports: 'Backbone'
      }
    }
  });

require.config()接受一个配置对象,这个对象除了有前面说过的paths属性之外,还有一个shim属性,专门用来配置不兼容的模块。具体来说,每个模块要定义(1)exports值(输出的变量名),表明这个模块外部调用时的名称;(2)deps数组,表明该模块的依赖性。
比如,jQuery的插件可以这样定义:

  shim: {
    'jquery.scroll': {
      deps: ['jquery'],
      exports: 'jQuery.fn.scroll'
    }
  }

作者:wuqke
链接:http://www.jianshu.com/p/b8a6...
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

查看原文

Druidiurd 收藏了文章 · 2018-06-05

[Webpack并不难]使用教程(一)--- entry,devtool,output,resolve

Webpack是什么,我就不过多介绍了,大家都有耳闻,不过还是配张图让大家体会下。
图片描述

我的webpack版本是 3.10.0

  • 安装Webpack可以全局安装和局部安装。局部安装的话就最好在安装的当前目录下运行,你硬要在在外部用webpack?那你在命令行要输入安装webpack位置的路径了。
npm install --save webpack // 这是局部安装
./node_modules/.bin/webpack --help // 局部安装的使用要带路径

哇,要写路径,好麻烦哦,没事,那就全局安装吧。

npm install -g webpack

现在用webpack一般都写好配置文件的了,webpack.config.js,那么接下来就说这个配置文件主要怎样写。

module.exports = {
  devtool: '#eval-source-map', // 这个是打包的方式
  entry: './main.js',          // 入口文件。支持数组形式,将加载数组中的所有模块,但以最后一个模块作为输出,对象形式也一样。
  output: {                    
    path: './js',              // 打包后的文件存放位置
    publicPath: '/dist/',      // 这个下面详说
    filename: 'build.js'       // 打包后的文件名
  },
  resolve: {                   // 查找module的话从这里开始查找,下面详说。
    root: 'D:/webpack-test/src',
    extensions: ['.js', '.json', '.scss'],
    alias: {
        // 下面有例子。
    }
  }
};

devtool:打包方式。(官网的文档

devtool选项打包速度重建速度是否支持生产模式
source-map--支持
eval-source-map-+不支持
cheap-module-source-map0-支持
cheap-module-eval-source-map0++不支持
cheap-source-map+0支持
cheap-eval-source-map+++不支持
eval++++++不支持

从上到下,打包速度越来越快,开发环境一般用eval-source-map,生产环境自行斟酌咯。毕竟打包越快,打包质量也就越差。还有,不知大家发现没,带eval都不支持生产模式哦。

publicPath官网的文档

它被用来更新内嵌到css、html文件里的url值。

上面 publicPath: '/dist/' ,例如:

background-image:url('./test.png) // 路径变为'/dist/.test.png'
path: '/js' // 上面打包后的文件位置,那么路径变为'/dist/js/build.js'

pubilcPath很重要。在生产模式下如“test.png”文件可能会定位到CDN上并且你的Node.js服务器可能是运行在HeroKu(一个支持多种编程语言的云平台)上边的。一张图片,手动修改下咯,那如果你网站有上百张呢,那publicPath:'你服务器的ip地址',这样省事很多吧。

resolve官网的文档

  1. root:包含您的模块的目录(绝对路径)。
  2. extensions: 加载模块时可忽略的扩展名。
  3. alias:模块别名定义。举些例子:

    'Abc': '/js/x/y/z/abc.js'  
    // require('Abc'); 相当于 require('/js/x/y/z/abc.js')
    // 如果 require('Abc/index.js'), 这样不行的。
    
    'Abc': './js/x/y/z/abc.js' 
    // 如果该值是一个相对路径,它将相对于包含require的文件。
    // 例如:在test.js中require('Abc'), 那么test.js和abc.js要在同目录下的。  
      
    'Abc': '/js/store'
    // require('Abc') 就相当于 require('/js/store/index.js')
    // require('Abc/other.js') 就相当于 require('/js/store/other.js')
      
    'Abc$': '/js/store'
    // require('Abc') 就相当于 require('/js/store/index.js')
    // 后面带有 $ ,意味着要完全匹配 'Abc'
    // 如果 require('Abc/other.js'),因为没完全匹配Abc,那么加载的是 node_modules下Abc文件夹里的other.js!!!

使用教程(二)--- module.loaders

查看原文

Druidiurd 收藏了文章 · 2018-06-05

重构 - 设计API的扩展机制

1.前言

上篇文章,主要介绍了重构的一些概念和一些简单的实例。这一次,详细的说下项目中的一个重构场景--给API设计扩展机制。目的就是为了方便以后能灵活应对需求的改变。当然了,是否需要设计扩展性这个要看API的需求。如果大家有什么建议,欢迎评论留言。

2.扩展性表现形式

2-1.prototype

这个可以说是JS里面最原的一个扩展。比如原生JS没有提供打乱数组顺序的API,但是开发者又想方便使用,这样的话,就只能扩展数组的prototype。代码如下

//扩展Array.prototype,增加打乱数组的方法。
Array.prototype.upset=function(){
    return this.sort((n1,n2)=>Math.random() - 0.5);
}

let arr=[1,2,3,4,5];
//调用
arr.upset();
//显示结果
console.log(arr);

运行结果

clipboard.png

功能是实现了。但是上面的代码,只想借用例子讲解扩展性,大家看下就好。不要模仿,也不要在项目这样写。现在基本都禁止这样开发了。理由也很简单,之前的文章也有提到过。这里重复一下。

这样就污染了原生对象Array,别人创建的Array也会被污染,造成不必要的开销。最可怕的是,万一自己命名的跟原生的方法重名了,就被覆盖原来的方法了。

Array.prototype.push=function(){console.log('守候')}  
let arrTest=[123]
arrTest.push()
//result:守候
//push方法有什么作用,大家应该知道,不知道的可以去w3c看下

clipboard.png

2-2.jQuery

关于 jQuery 的扩展性,分别提供了三个API:$.extend()、$.fn和$.fn.extend()。分别对jQuery的本身,静态方法,原型对象进行扩展,基于jQuery写插件的时候,最离不开的应该就是$.fn.extend()。

参考链接:

理解jquery的$.extend()、$.fn和$.fn.extend()
Jquery自定义插件之$.extend()、$.fn和$.fn.extend()

2-3.VUE

对VUE进行扩展,引用官网(插件)的说法,扩展的方式一般有以下几种:

1.添加全局方法或者属性,如: vue-custom-element

2.添加全局资源:指令/过滤器/过渡等,如 vue-touch

3.通过全局 mixin 方法添加一些组件选项,如: vue-router

4.添加 Vue 实例方法,通过把它们添加到 Vue.prototype 上实现。

5.一个库,提供自己的 API,同时提供上面提到的一个或多个功能,如 vue-router

基于VUE的扩展。在组件,插件的内容提供一个install方法。如下

clipboard.png

使用组件

clipboard.png

上面几个扩展性的实例分别是原生对象,库,框架的扩展,大家可能觉得有点夸夸而谈,那下面就分享一个日常开发常用的一个实例。

3.实例-表单验证

看了上面那些扩展性的实例,下面看下一个在日常开发使用得也很多的一个实例:表单验证。这块可以说很简单,但是做好,做通用不简单。看了《JavaScript设计模式与开发实践》,用策略模式对以前的表单验证函数进行了一个重构。下面进行一个简单的分析。

下面的内容,代码会偏多,虽然代码不难,但还是强烈建议大家不要只看,要边看,边写,边调试,不然作为读者,很可能不知道我的代码是什么意思,很容易懵。下面的代码回涉两个知识:开放-封闭原则和策略模式,大家可以自行了解。

3-1.原来方案

/**
 * @description 字段检验
 * @param checkArr
 * @returns {boolean}
 */
function validateForm(checkArr){
    let _reg = null, ruleMsg, nullMsg, lenMsg;
    for (let i = 0, len = checkArr.length; i < len; i++) {
        //如果没字段值是undefined,不再执行当前循环,执行下一次循环
        if (checkArr[i].el === undefined) {
            continue;
        }
        //设置规则错误提示信息
        ruleMsg = checkArr[i].msg || '字段格式错误';
        //设置值为空则错误提示信息
        nullMsg = checkArr[i].nullMsg || '字段不能为空';
        //设置长度错误提示信息
        lenMsg = checkArr[i].lenMsg || '字段长度范围' + checkArr[i].minLength + "至" + checkArr[i].maxLength;
        //如果该字段有空值校验
        if (checkArr[i].noNull === true) {
            //如果字段为空,返回结果又提示信息
            if (checkArr[i].el === "" || checkArr[i].el === null) {
                return nullMsg;
            }
        }
        //如果有该字段有规则校验
        if (checkArr[i].rule) {
            //设置规则
            switch (checkArr[i].rule) {
                case 'mobile':
                    _reg = /^1[3|4|5|7|8][0-9]\d{8}$/;
                    break;
                case 'tel':
                    _reg = /^\d{3}-\d{8}|\d{4}-\d{7}|\d{11}$/;
                    break;
            }
            //如果字段不为空,并且规则错误,返回错误信息
            if (!_reg.test(checkArr[i].el) && checkArr[i].el !== "" && checkArr[i].el !== null) {
                return ruleMsg;
            }
        }
        //如果字段不为空并且长度错误,返回错误信息
        if (checkArr[i].el !== null && checkArr[i].el !== '' && (checkArr[i].minLength || checkArr[i].maxLength)) {
            if (checkArr[i].el.toString().length < checkArr[i].minLength || checkArr[i].el.toString().length > checkArr[i].maxLength) {
                return lenMsg;
            }
        }
    }
    return false;
}

函数调用方式

    let testData={
        phone:'18819323632',
        pwd:'112'
    }

    let _tips = validateForm([
        {el: testData.phone, noNull: true, nullMsg: '电话号码不能为空',rule: "mobile", msg: '电话号码格式错误'},
        {el: testData.pwd, noNull: true, nullMsg: '密码不能为空',lenMsg:'密码长度不正确',minLength:6,maxLength:18}
    ]);
    //字段验证如果返回错误信息
    if (_tips) {
        alert(_tips);
    }

3-2.存在问题

这样方法,相信大家看的也难受,因为问题确实是比较多。

1.一个字段进入,可能要经过三种判断(空值,规则,长度)。如果只是一个简单的电话号码规则校验,就要经过其他两种没必要的校验,造成不必要的开销。运行的流程就如同下面。

图片描述

2.规则校验里面,只有这几种校验,如果要增加其他校验,比如增加一个日期的规则,无法完成。如果一直修改源码,可能会导致函数巨大。

3.写法不优雅,调用也不方便。

3-3.代替方案

针对上面2-2的三个问题,逐个进行改善。

因为调用方式就不方便,很难在不改变validateForm调用方式的同时,优化重构内部的代码,又增加扩展性。重写这个方法又不可能,因为有个别的地方已经使用了这个API,自己一个一个的改不现实,所以就不修改这个validateForm,新建一个新的API:validate。在以后的项目上,也尽量引导同事放弃validateForm,使用新的API。

上面第一个,优化校验规则,每次校验(比如空值,长度,规则),都是一个简单的校验,不再执行其他没必要的校验。运行流程如同下面。

图片描述

let validate = function (arr) {
    let ruleData = {
        /**
         * @description 不能为空
         * @param val
         * @param msg
         * @return {*}
         */
        isNoNull(val, msg){
            if (!val) {
                return msg
            }
        },
        /**
         * @description 最小长度
         * @param val
         * @param length
         * @param msg
         * @return {*}
         */
        minLength(val, length, msg){
            if (val.toString().length < length) {
                return msg
            }
        },
        /**
         * @description 最大长度
         * @param val
         * @param length
         * @param msg
         * @return {*}
         */
        maxLength(val, length, msg){
            if (val.toString().length > length) {
                return msg
            }
        },
        /**
         * @description 是否是手机号码格式
         * @param val
         * @param msg
         * @return {*}
         */
        isMobile(val, msg){
            if (!/^1[3-9]\d{9}$/.test(val)) {
                return msg
            }
        }
    }
    let ruleMsg, checkRule, _rule;
    for (let i = 0, len = arr.length; i < len; i++) {
        //如果字段找不到
        if (arr[i].el === undefined) {
            return '字段找不到!'
        }
        //遍历规则
        for (let j = 0; j < arr[i].rules.length; j++) {
            //提取规则
            checkRule = arr[i].rules[j].rule.split(":");
            _rule = checkRule.shift();
            checkRule.unshift(arr[i].el);
            checkRule.push(arr[i].rules[j].msg);
            //如果规则错误
            ruleMsg = ruleData[_rule].apply(null, checkRule);
            if (ruleMsg) {
                //返回错误信息
                return ruleMsg;
            }
        }
    }
};
let testData = {
    name: '',
    phone: '18819522663',
    pw: 'asda'
}
//校验函数调用
console.log(validate([
    {
        //校验的数据
        el: testData.phone,
        //校验的规则
        rules: [
            {rule: 'isNoNull', msg: '电话不能为空'}, {rule: 'isMobile', msg: '手机号码格式不正确'}
        ]
    },
    {
        el: testData.pw,
        rules: [
            {rule: 'isNoNull', msg: '电话不能为空'},
            {rule:'minLength:6',msg:'密码长度不能小于6'}
        ]
    }
]));

如果又有其它的规则,又得改这个,这样就违反了开放-封闭原则。如果多人共用这个函数,规则可能会很多,ruleData会变的巨大,造成不必要的开销。比如A页面有金额的校验,但是只有A页面有。如果按照上面的方式改,在B页面也会加载金额的校验规则,但是根本不会用上,造成资源浪费。

所以下面应用开放-封闭原则。给函数的校验规则增加扩展性。在实操之前,大家应该会懵,因为一个函数,可以进行校验的操作,又有增加校验规则的操作。一个函数做两件事,就违反了单一原则。到时候也难维护,所以推荐的做法就是分接口做。如下写法。

let validate = (function () {
    let ruleData = {
        /**
         * @description 不能为空
         * @param val
         * @param msg
         * @return {*}
         */
        isNoNull(val, msg){
            if (!val) {
                return msg
            }
        },
        /**
         * @description 最小长度
         * @param val
         * @param length
         * @param msg
         * @return {*}
         */
        minLength(val, length, msg){
            if (val.toString().length < length) {
                return msg
            }
        },
        /**
         * @description 最大长度
         * @param val
         * @param length
         * @param msg
         * @return {*}
         */
        maxLength(val, length, msg){
            if (val.toString().length > length) {
                return msg
            }
        },
        /**
         * @description 是否是手机号码格式
         * @param val
         * @param msg
         * @return {*}
         */
        isMobile(val, msg){
            if (!/^1[3-9]\d{9}$/.test(val)) {
                return msg
            }
        }
    }
    return {
        /**
         * @description 查询接口
         * @param arr
         * @return {*}
         */
        check: function (arr) {
            let ruleMsg, checkRule, _rule;
            for (let i = 0, len = arr.length; i < len; i++) {
                //如果字段找不到
                if (arr[i].el === undefined) {
                    return '字段找不到!'
                }
                //遍历规则
                for (let j = 0; j < arr[i].rules.length; j++) {
                    //提取规则
                    checkRule = arr[i].rules[j].rule.split(":");
                    _rule = checkRule.shift();
                    checkRule.unshift(arr[i].el);
                    checkRule.push(arr[i].rules[j].msg);
                    //如果规则错误
                    ruleMsg = ruleData[_rule].apply(null, checkRule);
                    if (ruleMsg) {
                        //返回错误信息
                        return ruleMsg;
                    }
                }
            }
        },
        /**
         * @description 添加规则接口
         * @param type
         * @param fn
         */
        addRule:function (type,fn) {
            ruleData[type]=fn;
        }
    }
})();
//校验函数调用-测试用例
console.log(validate.check([
    {
        //校验的数据
        el: testData.mobile,
        //校验的规则
        rules: [
            {rule: 'isNoNull', msg: '电话不能为空'}, {rule: 'isMobile', msg: '手机号码格式不正确'}
        ]
    },
    {
        el: testData.password,
        rules: [
            {rule: 'isNoNull', msg: '电话不能为空'},
            {rule:'minLength:6',msg:'密码长度不能小于6'}
        ]
    }
]));
//扩展-添加日期范围校验
validate.addRule('isDateRank',function (val,msg) {
    if(new Date(val[0]).getTime()>=new Date(val[1]).getTime()){
        return msg;
    }
});
//测试新添加的规则-日期范围校验
console.log(validate.check([
    {
        el:['2017-8-9 22:00:00','2017-8-8 24:00:00'],
        rules:[{
            rule:'isDateRank',msg:'日期范围不正确'
        }]
    }
    
]));

如上代码所示,这里需要往ruleData添加日期范围的校验,这里可以添加。但是不能访问和修改ruleData的东西,有一个保护的作用。还有一个就是,比如在A页面添加日期的校验,只在A页面存在,不会影响其它页面。如果日期的校验在其它地方都可能用上,就可以考虑,在全局里面为ruleData添加日期的校验的规则。

至于第三个问题,这样的想法,可能不算太优雅,调用也不是太方便,但是就我现在能想到的,这个就是最好方案啊了。

这个看似是已经做完了,但是大家可能觉得有一种情况没能应对,比如下面这种,做不到。

clipboard.png

因为上面的check接口,只要有一个错误了,就立马跳出了,不会校验下一个。如果要实现下面的功能,就得实现,如果有一个值校验错误,就记录错误信息,继续校验下一个,等到所有的校验都执行完了之后,如下面的流程图。

图片描述

代码上面(大家先忽略alias这个属性)

let validate= (function () {
    let ruleData = {
        /**
         * @description 不能为空
         * @param val
         * @param msg
         * @return {*}
         */
        isNoNull(val, msg){
            if (!val) {
                return msg
            }
        },
        /**
         * @description 最小长度
         * @param val
         * @param length
         * @param msg
         * @return {*}
         */
        minLength(val, length, msg){
            if (val.toString().length < length) {
                return msg
            }
        },
        /**
         * @description 最大长度
         * @param val
         * @param length
         * @param msg
         * @return {*}
         */
        maxLength(val, length, msg){
            if (val.toString().length > length) {
                return msg
            }
        },
        /**
         * @description 是否是手机号码格式
         * @param val
         * @param msg
         * @return {*}
         */
        isMobile(val, msg){
            if (!/^1[3-9]\d{9}$/.test(val)) {
                return msg
            }
        }
    }
    return {
        check: function (arr) {
            //代码不重复展示,上面一部分
        },
        addRule:function (type,fn) {
            //代码不重复展示,上面一部分
        },
        /**
         * @description 校验所有接口
         * @param arr
         * @return {*}
         */
        checkAll: function (arr) {
            let ruleMsg, checkRule, _rule,msgArr=[];
            for (let i = 0, len = arr.length; i < len; i++) {
                //如果字段找不到
                if (arr[i].el === undefined) {
                    return '字段找不到!'
                }
                //如果字段为空以及规则不是校验空的规则

                //遍历规则
                for (let j = 0; j < arr[i].rules.length; j++) {
                    //提取规则
                    checkRule = arr[i].rules[j].rule.split(":");
                    _rule = checkRule.shift();
                    checkRule.unshift(arr[i].el);
                    checkRule.push(arr[i].rules[j].msg);
                    //如果规则错误
                    ruleMsg = ruleData[_rule].apply(null, checkRule);
                    if (ruleMsg) {
                        //记录错误信息
                        msgArr.push({
                            el:arr[i].el,
                            alias:arr[i].alias,
                            rules:_rule,
                            msg:ruleMsg
                        });
                    }
                }
            }
            //返回错误信息
            return msgArr.length>0?msgArr:false;
        }
    }
})();
let testData = {
    name: '',
    phone: '188',
    pw: 'asda'
}
//扩展-添加日期范围校验
validate.addRule('isDateRank',function (val,msg) {
    if(new Date(val[0]).getTime()>=new Date(val[1]).getTime()){
        return msg;
    }
});
//校验函数调用
console.log(validate.checkAll([
    {
        //校验的数据
        el: testData.phone,
        alias:'mobile',
        //校验的规则
        rules: [
            {rule: 'isNoNull', msg: '电话不能为空'}, {rule: 'isMobile', msg: '手机号码格式不正确'},{rule:'minLength:6',msg: '手机号码不能少于6'}
        ]
    },
    {
        el: testData.pw,
        alias:'pwd',
        rules: [
            {rule: 'isNoNull', msg: '电话不能为空'},
            {rule:'minLength:6',msg:'密码长度不能小于6'}
        ]
    },
    {
        el:['2017-8-9 22:00:00','2017-8-8 24:00:00'],
        rules:[{
            rule:'isDateRank',msg:'日期范围不正确'
        }]
    }
]));

看到结果,现在所有的不合法的数据的记录都返回回来了。至于当时alias现在揭晓用处。
比如页面是vue渲染的,根据alias可以这样处理。

clipboard.png

图片描述

如果是jQuery渲染的,根据alias可以这样处理。

图片描述

图片描述

3-4.向下兼容方案

因为项目之前有使用了以前的校验API,不能一道切,在以前的API没废弃之前,不能影响之前的使用。所以要重写以前的validateForm,使之兼容现在的新API:validate。

    let validateForm=function (arr) {
        let _param=[],_single={};
        for(let i=0;i<arr.length;i++){
            _single={};
            _single.el=arr[i].el;
            _single.rules=[];
            //如有有非空检验
            if(arr[i].noNull){
                _single.rules.push({
                    rule: 'isNoNull',
                    msg: arr[i].nullMsg||'字段不能为空'
                })
            }
            //如果有最小长度校验
            if(arr[i].minLength){
                _single.rules.push({
                    rule: 'minLength:'+arr[i].minLength,
                    msg: arr[i].lenMsg ||'字段长度范围错误'
                })
            }
            //如果有最大长度校验
            if(arr[i].maxLength){
                _single.rules.push({
                    rule: 'maxLength:'+arr[i].maxLength,
                    msg: arr[i].lenMsg ||'字段长度范围错误'
                })
            }
            //如果有规则校验
            //校验转换规则
            let _ruleData={
                mobile:'isMobile'
            }
            if(arr[i].rule){
                _single.rules.push({
                    rule: _ruleData[arr[i].rule],
                    msg: arr[i].msg ||'字段格式错误'
                })
            }
            _param.push(_single);
        }
        let _result=validate.check(_param);
        return _result?_result:false;
    }
    let testData={
        phone:'18819323632',
        pwd:'112'
    }
    let _tips = validateForm([
        {el: testData.phone, noNull: true, nullMsg: '电话号码不能为空',rule: "mobile", msg: '电话号码格式错误'},
        {el: testData.pwd, noNull: true, nullMsg: '密码不能为空',lenMsg:'密码长度不正确',minLength:6,maxLength:18}
    ]);
    console.log(_tips)

4.小结

今天的例子就到这里了,这个例子,无非就是给API增加扩展性。这个例子比较简单,不算难。大家用这个代码在浏览器上运行,就很好理解。如果大家对这个例子有什么更好的建议,或者代码上有什么问题,欢迎在评论区留言,大家多交流,相互学习。

-------------------------华丽的分割线--------------------

想了解更多,关注关注我的微信公众号:守候书阁

clipboard.png

查看原文

Druidiurd 收藏了文章 · 2018-06-05

vue-cli脚手架中webpack配置基础文件详解

一、前言

vue-cli是构建vue单页应用的脚手架,输入一串指定的命令行从而自动生成vue.js+webpack的项目模板。这其中webpack发挥了很大的作用,它使得我们的代码模块化,引入一些插件帮我们完善功能可以将文件打包压缩,图片转base64等。后期对项目的配置使得我们对于脚手架自动生成的代码的理解更为重要,接下来我将基于webpack3.6.0版本结合文档将文件各个击破,纯干料。
重点章节点击查看:package.jsonconfig/index.jswebpack.base.conf.jswebpack.dev.conf.jswebpack.prod.conf.js

二、主体结构

├─build 
├─config 
├─dist
├─node_modules
├─src
│ ├─assets
│ ├─components
│ ├─router
│ ├─App.vue
│ ├─main.js
├─static
├─.babelrc
├─.editorconfig
├─.gitignore
├─.postcssrc.js
├─index.html
├─package-lock.json
├─package.json
└─README.md

1、 package.json

项目作为一个大家庭,每个文件都各司其职。package.json来制定名单,需要哪些npm包来参与到项目中来,npm install命令根据这个配置文件增减来管理本地的安装包。

{
//从name到private都是package的配置信息,也就是我们在脚手架搭建中输入的项目描述
  "name": "shop",//项目名称:不能以.(点)或者_(下划线)开头,不能包含大写字母,具有明确的的含义与现有项目名字不重复
  "version": "1.0.0",//项目版本号:遵循“大版本.次要版本.小版本”
  "description": "A Vue.js project",//项目描述
  "author": "qietuniu",//作者名字
  "private": true,//是否私有
  //scripts中的子项即是我们在控制台运行的脚本的缩写
  "scripts": {
   //①webpack-dev-server:启动了http服务器,实现实时编译;
   //inline模式会在webpack.config.js入口配置中新增webpack-dev-server/client?http://localhost:8080/的入口,使得我们访问路径为localhost:8080/index.html(相应的还有另外一种模式Iframe);
   //progress:显示打包的进度
    "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",  
    "start": "npm run dev",//与npm run dev相同,直接运行开发环境
    "build": "node build/build.js"//使用node运行build文件
  },
  //②dependencies(项目依赖库):在安装时使用--save则写入到dependencies
  "dependencies": {
    "vue": "^2.5.2",//vue.js
    "vue-router": "^3.0.1"//vue的路由插件
  },
  //和devDependencies(开发依赖库):在安装时使用--save-dev将写入到devDependencies
  "devDependencies": {
    "autoprefixer": "^7.1.2",//autoprefixer作为postcss插件用来解析CSS补充前缀,例如 display: flex会补充为display:-webkit-box;display: -webkit-flex;display: -ms-flexbox;display: flex。
    //babel:以下几个babel开头的都是针对es6解析的插件。用最新标准编写的 JavaScript 代码向下编译成可以在今天随处可用的版本
    "babel-core": "^6.22.1",//babel的核心,把 js 代码分析成 ast ,方便各个插件分析语法进行相应的处理。
    "babel-helper-vue-jsx-merge-props": "^2.0.3",//预制babel-template函数,提供给vue,jsx等使用
    "babel-loader": "^7.1.1",//使项目运行使用Babel和webpack来传输js文件,使用babel-core提供的api进行转译
    "babel-plugin-syntax-jsx": "^6.18.0",//支持jsx
    "babel-plugin-transform-runtime": "^6.22.0",//避免编译输出中的重复,直接编译到build环境中
    "babel-plugin-transform-vue-jsx": "^3.5.0",//babel转译过程中使用到的插件,避免重复
    "babel-preset-env": "^1.3.2",//转为es5,transform阶段使用到的插件之一
    "babel-preset-stage-2": "^6.22.0",//ECMAScript第二阶段的规范
    "chalk": "^2.0.1",//用来在命令行输出不同颜色文字
    "copy-webpack-plugin": "^4.0.1",//拷贝资源和文件
    "css-loader": "^0.28.0",//webpack先用css-loader加载器去解析后缀为css的文件,再使用style-loader生成一个内容为最终解析完的css代码的style标签,放到head标签里
    "extract-text-webpack-plugin": "^3.0.0",//将一个以上的包里面的文本提取到单独文件中
    "file-loader": "^1.1.4",//③打包压缩文件,与url-loader用法类似
    "friendly-errors-webpack-plugin": "^1.6.1",//识别某些类别的WebPACK错误和清理,聚合和优先排序,以提供更好的开发经验
    "html-webpack-plugin": "^2.30.1",//简化了HTML文件的创建,引入了外部资源,创建html的入口文件,可通过此项进行多页面的配置
    "node-notifier": "^5.1.2",//支持使用node发送跨平台的本地通知
    "optimize-css-assets-webpack-plugin": "^3.2.0",//压缩提取出的css,并解决ExtractTextPlugin分离出的js重复问题(多个文件引入同一css文件)
    "ora": "^1.2.0",//加载(loading)的插件
    "portfinder": "^1.0.13",//查看进程端口
    "postcss-import": "^11.0.0",//可以消耗本地文件、节点模块或web_modules
    "postcss-loader": "^2.0.8",//用来兼容css的插件
    "postcss-url": "^7.2.1",//URL上重新定位、内联或复制
    "rimraf": "^2.6.0",//节点的UNIX命令RM—RF,强制删除文件或者目录的命令
    "semver": "^5.3.0",//用来对特定的版本号做判断的
    "shelljs": "^0.7.6",//使用它来消除shell脚本在UNIX上的依赖性,同时仍然保留其熟悉和强大的命令,即可执行Unix系统命令
    "uglifyjs-webpack-plugin": "^1.1.1",//压缩js文件
    "url-loader": "^0.5.8",//压缩文件,可将图片转化为base64
    "vue-loader": "^13.3.0",//VUE单文件组件的WebPACK加载器
    "vue-style-loader": "^3.0.1",//类似于样式加载程序,您可以在CSS加载器之后将其链接,以将CSS动态地注入到文档中作为样式标签
    "vue-template-compiler": "^2.5.2",//这个包可以用来预编译VUE模板到渲染函数,以避免运行时编译开销和CSP限制
    "webpack": "^3.6.0",//打包工具
    "webpack-bundle-analyzer": "^2.9.0",//可视化webpack输出文件的大小
    "webpack-dev-server": "^2.9.1",//提供一个提供实时重载的开发服务器
    "webpack-merge": "^4.1.0"//它将数组和合并对象创建一个新对象。如果遇到函数,它将执行它们,通过算法运行结果,然后再次将返回的值封装在函数中
  },
  //engines是引擎,指定node和npm版本
  "engines": {
    "node": ">= 6.0.0",
    "npm": ">= 3.0.0"
  },
  //限制了浏览器或者客户端需要什么版本才可运行
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not ie <= 8"
  ]
}

注释:
①、点这里→webpack运行时的配置文档传送门

②、devDependencies和dependencies的区别: devDependencies里面的插件只用于开发环境,不用于生产环境,即辅助作用,打包的时候需要,打包完成就不需要了。而dependencies是需要发布到生产环境的,自始至终都在。比如webpack等只是在开发中使用的包就写入到devDependencies,而像vue这种项目全程依赖的包要写入到dependencies
点这里→更多安装包文档搜索页传送门

③、file-loader和url-loader的区别:以图片为例,file-loader可对图片进行压缩,但是还是通过文件路径进行引入,当http请求增多时会降低页面性能,而url-loader通过设定limit参数,小于limit字节的图片会被转成base64的文件,大于limit字节的将进行图片压缩的操作。总而言之,url-loader是file-loader的上层封装。
点这里→file-loader 和 url-loader详解
点这里→file-loader文档传送门
点这里→url-loader文档传送门

2、.postcssrc.js

.postcssrc.js文件其实是postcss-loader包的一个配置,在webpack的旧版本可以直接在webpack.config.js中配置,现版本中postcss的文档示例独立出.postcssrc.js,里面写进去需要使用到的插件

module.exports = {
  "plugins": {
    "postcss-import": {},//①
    "postcss-url": {},//②
    "autoprefixer": {}//③
  }
}

注释:
①、点这里→postcss-import文档传送门
②、点这里→postcss-url文档传送门
③、点这里→autoprefixer文档传送门

3、 .babelrc

该文件是es6解析的一个配置

{
//制定转码的规则
  "presets": [
  //env是使用babel-preset-env插件将js进行转码成es5,并且设置不转码的AMD,COMMONJS的模块文件,制定浏览器的兼容
    ["env", {
      "modules": false,
      "targets": {
        "browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
      }
    }],
    "stage-2"
  ],
  
  "plugins": ["transform-vue-jsx", "transform-runtime"]//①
}

注释:
①、点这里→transform-vue-jsx文档传送门
点这里→transform-runtime文档传送门

4、src内文件

我们开发的代码都存放在src目录下,根据需要我们通常会再建一些文件夹。比如pages的文件夹,用来存放页面让components文件夹专门做好组件的工作;api文件夹,来封装请求的参数和方法;store文件夹,使用vuex来作为vue的状态管理工具,我也常叫它作前端的数据库等。


①、assets文件:脚手架自动会放入一个图片在里面作为初始页面的logo。平常我们使用的时候会在里面建立js,css,img,fonts等文件夹,作为静态资源调用

②、components文件夹:用来存放组件,合理地使用组件可以高效地实现复用等功能,从而更好地开发项目。一般情况下比如创建头部组件的时候,我们会新建一个header的文件夹,然后再新建一个header.vue的文件

③、router文件夹:该文件夹下有一个叫index.js文件,用于实现页面的路由跳转,具体使用请点击→vue-router传送门

④、App.vue:作为我们的主组件,可通过使用<router-view/>开放入口让其他的页面组件得以显示。

⑤、main.js:作为我们的入口文件,主要作用是初始化vue实例并使用需要的插件,小型项目省略router时可放在该处

注释:具体vue的用法可查看vue官方中文文档传送门

5、其他文件

①、.editorconfig:编辑器的配置文件

②、.gitignore:忽略git提交的一个文件,配置之后提交时将不会加载忽略的文件

③、index.html:页面入口,经过编译之后的代码将插入到这来。

④、package.lock.json:锁定安装时的包的版本号,并且需要上传到git,以保证其他人在npm install时大家的依赖能保证一致

⑤、README.md:可此填写项目介绍

⑥、node_modules:根据package.json安装时候生成的的依赖(安装包)

三、config文件夹

├─config 
│ ├─dev.env.js 
│ ├─index.js 
│ ├─prod.env.js 

1、config/dev.env.js

config内的文件其实是服务于build的,大部分是定义一个变量export出去。

'use strict'//采用严格模式
const merge = require('webpack-merge')//①
const prodEnv = require('./prod.env')
//webpack-merge提供了一个合并函数,它将数组和合并对象创建一个新对象。
//如果遇到函数,它将执行它们,通过算法运行结果,然后再次将返回的值封装在函数中.这边将dev和prod进行合并
module.exports = merge(prodEnv, {
  NODE_ENV: '"development"'
})

注释:①、点这里→webpack-merge文档传送门

2、config/prod.env.js

当开发是调取dev.env.js的开发环境配置,发布时调用prod.env.js的生产环境配置

'use strict'
module.exports = {
  NODE_ENV: '"production"'
}

3、config/index.js

'use strict'
const path = require('path')

module.exports = {
  dev: {
    // 开发环境下面的配置
    assetsSubDirectory: 'static',//子目录,一般存放css,js,image等文件
    assetsPublicPath: '/',//根目录
    proxyTable: {},//可利用该属性解决跨域的问题
    host: 'localhost', // 地址
    port: 8080, //端口号设置,端口号占用出现问题可在此处修改
    autoOpenBrowser: false,//是否在编译(输入命令行npm run dev)后打开http://localhost:8080/页面,以前配置为true,近些版本改为false,个人偏向习惯自动打开页面
    errorOverlay: true,//浏览器错误提示
    notifyOnErrors: true,//跨平台错误提示
    poll: false, //使用文件系统(file system)获取文件改动的通知devServer.watchOptions
    devtool: 'cheap-module-eval-source-map',//增加调试,该属性为原始源代码(仅限行)不可在生产环境中使用
    cacheBusting: true,//使缓存失效
    cssSourceMap: true//代码压缩后进行调bug定位将非常困难,于是引入sourcemap记录压缩前后的位置信息记录,当产生错误时直接定位到未压缩前的位置,将大大的方便我们调试
  },

  build: {
  // 生产环境下面的配置
    index: path.resolve(__dirname, '../dist/index.html'),//index编译后生成的位置和名字,根据需要改变后缀,比如index.php
    assetsRoot: path.resolve(__dirname, '../dist'),//编译后存放生成环境代码的位置
    assetsSubDirectory: 'static',//js,css,images存放文件夹名
    assetsPublicPath: '/',//发布的根目录,通常本地打包dist后打开文件会报错,此处修改为./。如果是上线的文件,可根据文件存放位置进行更改路径
    productionSourceMap: true,
    devtool: '#source-map',//①
    //unit的gzip命令用来压缩文件,gzip模式下需要压缩的文件的扩展名有js和css
    productionGzip: false,
    productionGzipExtensions: ['js', 'css'],
    bundleAnalyzerReport: process.env.npm_config_report
  }
}

注释:①点击→devtool文档传送门

四、build文件夹

├─build 
│ ├─build.js 
│ ├─check-versions.js 
│ ├─utils.js 
│ ├─vue-loader.conf.js 
│ ├─webpack.base.conf.js 
│ ├─webpack.dev.conf.js 
│ ├─webpack.prod.conf.js 

1、build/build.js

该文件作用,即构建生产版本。package.json中的scripts的build就是node build/build.js,输入命令行npm run build对该文件进行编译生成生产环境的代码。

'use strict'
require('./check-versions')()//check-versions:调用检查版本的文件。加()代表直接调用该函数
process.env.NODE_ENV = 'production'//设置当前是生产环境
//下面定义常量引入插件
const ora = require('ora')//①加载动画
const rm = require('rimraf')//②删除文件
const path = require('path')
const chalk = require('chalk')//③对文案输出的一个彩色设置
const webpack = require('webpack')
const config = require('../config')//默认读取下面的index.js文件
const webpackConfig = require('./webpack.prod.conf')
//调用start的方法实现加载动画,优化用户体验
const spinner = ora('building for production...')
spinner.start()
//先删除dist文件再生成新文件,因为有时候会使用hash来命名,删除整个文件可避免冗余
rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
  if (err) throw err
  webpack(webpackConfig, (err, stats) => {
    spinner.stop()
    if (err) throw err
    process.stdout.write(stats.toString({
      colors: true,
      modules: false,
      children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build.
      chunks: false,
      chunkModules: false
    }) + '\n\n')

    if (stats.hasErrors()) {
      process.exit(1)
    }

    console.log(chalk.cyan('  Build complete.\n'))
    console.log(chalk.yellow(
      '  Tip: built files are meant to be served over an HTTP server.\n' +
      '  Opening index.html over file:// won\'t work.\n'
    ))
  })
})

注释:
①、点这里→ora文档传送门
②、点这里→chalk文档传送门
③、点这里→rimraf文档传送门

2、build/check-version.js

该文件用于检测node和npm的版本,实现版本依赖

'use strict'
const chalk = require('chalk')
const semver = require('semver')//①对版本进行检查
const packageConfig = require('../package.json')
const shell = require('shelljs')

function exec (cmd) {
//返回通过child_process模块的新建子进程,执行 Unix 系统命令后转成没有空格的字符串
  return require('child_process').execSync(cmd).toString().trim()
}

const versionRequirements = [
  {
    name: 'node',
    currentVersion: semver.clean(process.version),//使用semver格式化版本
    versionRequirement: packageConfig.engines.node//获取package.json中设置的node版本
  }
]

if (shell.which('npm')) {
  versionRequirements.push({
    name: 'npm',
    currentVersion: exec('npm --version'),// 自动调用npm --version命令,并且把参数返回给exec函数,从而获取纯净的版本号
    versionRequirement: packageConfig.engines.npm
  })
}

module.exports = function () {
  const warnings = []
  for (let i = 0; i < versionRequirements.length; i++) {
    const mod = versionRequirements[i]

    if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
    //上面这个判断就是如果版本号不符合package.json文件中指定的版本号,就执行下面错误提示的代码
      warnings.push(mod.name + ': ' +
        chalk.red(mod.currentVersion) + ' should be ' +
        chalk.green(mod.versionRequirement)
      )
    }
  }

  if (warnings.length) {
    console.log('')
    console.log(chalk.yellow('To use this template, you must update following to modules:'))
    console.log()

    for (let i = 0; i < warnings.length; i++) {
      const warning = warnings[i]
      console.log('  ' + warning)
    }

    console.log()
    process.exit(1)
  }
}

注释:
①、点这里→chalk文档传送门
点这里→semver文档传送门

3、build/utils.js

utils是工具的意思,是一个用来处理css的文件。

'use strict'
const path = require('path')
const config = require('../config')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const packageConfig = require('../package.json')
//导出文件的位置,根据环境判断开发环境和生产环境,为config文件中index.js文件中定义的build.assetsSubDirectory或dev.assetsSubDirectory
exports.assetsPath = function (_path) {
  const assetsSubDirectory = process.env.NODE_ENV === 'production'
    ? config.build.assetsSubDirectory
    : config.dev.assetsSubDirectory
//Node.js path 模块提供了一些用于处理文件路径的小工具①
  return path.posix.join(assetsSubDirectory, _path)
}

exports.cssLoaders = function (options) {
  options = options || {}
//使用了css-loader和postcssLoader,通过options.usePostCSS属性来判断是否使用postcssLoader中压缩等方法
  const cssLoader = {
    loader: 'css-loader',
    options: {
      sourceMap: options.sourceMap
    }
  }

  const postcssLoader = {
    loader: 'postcss-loader',
    options: {
      sourceMap: options.sourceMap
    }
  }
  function generateLoaders (loader, loaderOptions) {
    const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader]
    if (loader) {
      loaders.push({
        loader: loader + '-loader',
        //Object.assign是es6语法的浅复制,后两者合并后复制完成赋值
        options: Object.assign({}, loaderOptions, {
          sourceMap: options.sourceMap
        })
      })
    }
    
    if (options.extract) {
    //ExtractTextPlugin可提取出文本,代表首先使用上面处理的loaders,当未能正确引入时使用vue-style-loader
      return ExtractTextPlugin.extract({
        use: loaders,
        fallback: 'vue-style-loader'
      })
    } else {
    //返回vue-style-loader连接loaders的最终值
      return ['vue-style-loader'].concat(loaders)
    }
  }
  return {
    css: generateLoaders(),//需要css-loader 和 vue-style-loader
    postcss: generateLoaders(),//需要css-loader和postcssLoader  和 vue-style-loader
    less: generateLoaders('less'),//需要less-loader 和 vue-style-loader
    sass: generateLoaders('sass', { indentedSyntax: true }),//需要sass-loader 和 vue-style-loader
    scss: generateLoaders('sass'),//需要sass-loader 和 vue-style-loader
    stylus: generateLoaders('stylus'),//需要stylus-loader 和 vue-style-loader
    styl: generateLoaders('stylus')//需要stylus-loader 和 vue-style-loader
  }
}
exports.styleLoaders = function (options) {
  const output = []
  const loaders = exports.cssLoaders(options)
    //将各种css,less,sass等综合在一起得出结果输出output
  for (const extension in loaders) {
    const loader = loaders[extension]
    output.push({
      test: new RegExp('\\.' + extension + '$'),
      use: loader
    })
  }

  return output
}

exports.createNotifierCallback = () => {
//发送跨平台通知系统
  const notifier = require('node-notifier')

  return (severity, errors) => {
    if (severity !== 'error') return
//当报错时输出错误信息的标题,错误信息详情,副标题以及图标
    const error = errors[0]
    const filename = error.file && error.file.split('!').pop()

    notifier.notify({
      title: packageConfig.name,
      message: severity + ': ' + error.name,
      subtitle: filename || '',
      icon: path.join(__dirname, 'logo.png')
    })
  }
}

注释:
①、path.posix:提供对路径方法的POSIX(可移植性操作系统接口)特定实现的访问,即可跨平台,区别于win32。
path.join:用于连接路径,会正确使用当前系统的路径分隔符,Unix系统是"/",Windows系统是""
点击→path用法传送门

4、vue-loader.conf.js

该文件的主要作用就是处理.vue文件,解析这个文件中的每个语言块(template、script、style),转换成js可用的js模块。

'use strict'
const utils = require('./utils')
const config = require('../config')
const isProduction = process.env.NODE_ENV === 'production'
const sourceMapEnabled = isProduction
  ? config.build.productionSourceMap
  : config.dev.cssSourceMap
//处理项目中的css文件,生产环境和测试环境默认是打开sourceMap,而extract中的提取样式到单独文件只有在生产环境中才需要
module.exports = {
  loaders: utils.cssLoaders({
    sourceMap: sourceMapEnabled,
    extract: isProduction
  }),
  cssSourceMap: sourceMapEnabled,
  cacheBusting: config.dev.cacheBusting,
   // 在模版编译过程中,编译器可以将某些属性,如 src 路径,转换为require调用,以便目标资源可以由 webpack 处理.
  transformToRequire: {
    video: ['src', 'poster'],
    source: 'src',
    img: 'src',
    image: 'xlink:href'
  }
}

5、webpack.base.conf.js

webpack.base.conf.js是开发和生产共同使用提出来的基础配置文件,主要实现配制入口,配置输出环境,配置模块resolve和插件等

'use strict'
const path = require('path')
const utils = require('./utils')
const config = require('../config')
const vueLoaderConfig = require('./vue-loader.conf')

function resolve (dir) {
//拼接出绝对路径
  return path.join(__dirname, '..', dir)
}
module.exports = {
//path.join将路径片段进行拼接,而path.resolve将以/开始的路径片段作为根目录,在此之前的路径将会被丢弃
//path.join('/a', '/b') // 'a/b',path.resolve('/a', '/b') // '/b'
  context: path.resolve(__dirname, '../'),
  //配置入口,默认为单页面所以只有app一个入口
  entry: {
    app: './src/main.js'
  },
  //配置出口,默认是/dist作为目标文件夹的路径
  output: {
    path: config.build.assetsRoot,//路径
    filename: '[name].js',//文件名
    publicPath: process.env.NODE_ENV === 'production'
      ? config.build.assetsPublicPath
      : config.dev.assetsPublicPath//公共存放路径
  },
  resolve: {
  //自动的扩展后缀,比如一个js文件,则引用时书写可不要写.js
    extensions: ['.js', '.vue', '.json'],
    //创建路径的别名,比如增加'components': resolve('src/components')等
    alias: {
      'vue$': 'vue/dist/vue.esm.js',
      '@': resolve('src'),
    }
  },
  //使用插件配置相应文件的处理方法
  module: {
    rules: [
    //使用vue-loader将vue文件转化成js的模块①
      {
        test: /\.vue$/,
        loader: 'vue-loader',
        options: vueLoaderConfig
      },
      //js文件需要通过babel-loader进行编译成es5文件以及压缩等操作②
      {
        test: /\.js$/,
        loader: 'babel-loader',
        include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')]
      },
      //图片、音像、字体都使用url-loader进行处理,超过10000会编译成base64③
      {
        test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
        loader: 'url-loader',
        options: {
          limit: 10000,
          name: utils.assetsPath('img/[name].[hash:7].[ext]')
        }
      },
      {
        test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
        loader: 'url-loader',
        options: {
          limit: 10000,
          name: utils.assetsPath('media/[name].[hash:7].[ext]')
        }
      },
      {
        test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
        loader: 'url-loader',
        options: {
          limit: 10000,
          name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
        }
      }
    ]
  },
  //以下选项是Node.js全局变量或模块,这里主要是防止webpack注入一些Node.js的东西到vue中 
  node: 
    setImmediate: false,
    dgram: 'empty',
    fs: 'empty',
    net: 'empty',
    tls: 'empty',
    child_process: 'empty'
  }
}

注释:
①、点击→vue-loader文档传送门
②、点击→babel-loader文档传送门

6、webpack.dev.conf.js

'use strict'
const utils = require('./utils')
const webpack = require('webpack')
const config = require('../config')
//通过webpack-merge实现webpack.dev.conf.js对webpack.base.config.js的继承
const merge = require('webpack-merge')
const path = require('path')
const baseWebpackConfig = require('./webpack.base.conf')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
//美化webpack的错误信息和日志的插件①
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
const portfinder = require('portfinder')// 查看空闲端口位置,默认情况下搜索8000这个端口②
const HOST = process.env.HOST//③processs为node的一个全局对象获取当前程序的环境变量,即host
const PORT = process.env.PORT && Number(process.env.PORT)

const devWebpackConfig = merge(baseWebpackConfig, {
  module: {
  //规则是工具utils中处理出来的styleLoaders,生成了css,less,postcss等规则
    rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true })
  },

  devtool: config.dev.devtool,  //增强调试,上文有提及
  //此处的配置都是在config的index.js中设定好了
  devServer: {//④
    clientLogLevel: 'warning',//控制台显示的选项有none, error, warning 或者 info
    //当使用 HTML5 History API 时,任意的 404 响应都可能需要被替代为 index.html
    historyApiFallback: {
      rewrites: [
        { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') },
      ],
    },
    hot: true,//热加载
    contentBase: false,
    compress: true,//压缩
    host: HOST || config.dev.host,
    port: PORT || config.dev.port,
    open: config.dev.autoOpenBrowser,//调试时自动打开浏览器
    overlay: config.dev.errorOverlay
      ? { warnings: false, errors: true }
      : false,// warning 和 error 都要显示
    publicPath: config.dev.assetsPublicPath,
    proxy: config.dev.proxyTable,//接口代理
    quiet: true, //控制台是否禁止打印警告和错误,若用FriendlyErrorsPlugin 此处为 true
    watchOptions: {
      poll: config.dev.poll,//// 文件系统检测改动
    }
  },
  plugins: [
    new webpack.DefinePlugin({
      'process.env': require('../config/dev.env')
    }),
    new webpack.HotModuleReplacementPlugin(),//⑤模块热替换插件,修改模块时不需要刷新页面
    new webpack.NamedModulesPlugin(), // 显示文件的正确名字
    new webpack.NoEmitOnErrorsPlugin(),//当webpack编译错误的时候,来中端打包进程,防止错误代码打包到文件中
    // https://github.com/ampedandwired/html-webpack-plugin
    // 该插件可自动生成一个 html5 文件或使用模板文件将编译好的代码注入进去⑥
    new HtmlWebpackPlugin({
      filename: 'index.html',
      template: 'index.html',
      inject: true
    }),
    new CopyWebpackPlugin([//复制插件
      {
        from: path.resolve(__dirname, '../static'),
        to: config.dev.assetsSubDirectory,
        ignore: ['.*']//忽略.*的文件
      }
    ])
  ]
})

module.exports = new Promise((resolve, reject) => {
  portfinder.basePort = process.env.PORT || config.dev.port
  //查找端口号
  portfinder.getPort((err, port) => {
    if (err) {
      reject(err)
    } else {
    //端口被占用时就重新设置evn和devServer的端口
      process.env.PORT = port
      devWebpackConfig.devServer.port = port
      //友好地输出信息
      devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({
        compilationSuccessInfo: {
          messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`],
        },
        onErrors: config.dev.notifyOnErrors
        ? utils.createNotifierCallback()
        : undefined
      }))
      resolve(devWebpackConfig)
    }
  })
})

注释:
①、点击→friendly-errors-webpack-plugin文档传送门
②、点击→process文档传送门
③、点击→babel-loader文档传送门
④、点击→devtool文档传送门
⑤、点击→webpack的HotModuleReplacementPlugin文档传送门
⑥、点击→html-webpack-plugin文档传送门

7、webpack.prod.conf.js

'use strict'
const path = require('path')
const utils = require('./utils')
const webpack = require('webpack')
const config = require('../config')
const merge = require('webpack-merge')
const baseWebpackConfig = require('./webpack.base.conf')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')

const env = require('../config/prod.env')

const webpackConfig = merge(baseWebpackConfig, {
  module: {
  //调用utils.styleLoaders的方法
    rules: utils.styleLoaders({
      sourceMap: config.build.productionSourceMap,//开启调试的模式。默认为true
      extract: true,
      usePostCSS: true
    })
  },
  devtool: config.build.productionSourceMap ? config.build.devtool : false,
  output: {
    path: config.build.assetsRoot,
    filename: utils.assetsPath('js/[name].[chunkhash].js'),
    chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
  },
  plugins: [
    new webpack.DefinePlugin({
      'process.env': env
    }),
    new UglifyJsPlugin({
      uglifyOptions: {
        compress: {//压缩
          warnings: false//警告:true保留警告,false不保留
        }
      },
      sourceMap: config.build.productionSourceMap,
      parallel: true
    }),
    new ExtractTextPlugin({//抽取文本。比如打包之后的index页面有style插入,就是这个插件抽取出来的,减少请求
      filename: utils.assetsPath('css/[name].[contenthash].css'),  
      allChunks: true,
    }),
    
    new OptimizeCSSPlugin({//优化css的插件
      cssProcessorOptions: config.build.productionSourceMap
        ? { safe: true, map: { inline: false } }
        : { safe: true }
    }),
   
    new HtmlWebpackPlugin({//html打包
      filename: config.build.index,
      template: 'index.html',
      inject: true,
      minify: {//压缩
        removeComments: true,//删除注释
        collapseWhitespace: true,//删除空格
        removeAttributeQuotes: true//删除属性的引号   
      },
     
      chunksSortMode: 'dependency'//模块排序,按照我们需要的顺序排序
    }),
   
    new webpack.HashedModuleIdsPlugin(),
    new webpack.optimize.ModuleConcatenationPlugin(),
    new webpack.optimize.CommonsChunkPlugin({//抽取公共的模块
      name: 'vendor',
      minChunks (module) {   
        return (
          module.resource &&
          /\.js$/.test(module.resource) &&
          module.resource.indexOf(
            path.join(__dirname, '../node_modules')
          ) === 0
        )
      }
    }),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'manifest',
      minChunks: Infinity
    }),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'app',
      async: 'vendor-async',
      children: true,
      minChunks: 3
    }),
    new CopyWebpackPlugin([//复制,比如打包完之后需要把打包的文件复制到dist里面
      {
        from: path.resolve(__dirname, '../static'),
        to: config.build.assetsSubDirectory,
        ignore: ['.*']
      }
    ])
  ]
})

if (config.build.productionGzip) {
  const CompressionWebpackPlugin = require('compression-webpack-plugin')

  webpackConfig.plugins.push(
    new CompressionWebpackPlugin({
      asset: '[path].gz[query]',
      algorithm: 'gzip',
      test: new RegExp(
        '\\.(' +
        config.build.productionGzipExtensions.join('|') +
        ')$'
      ),
      threshold: 10240,
      minRatio: 0.8
    })
  )
}

if (config.build.bundleAnalyzerReport) {
  const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
  webpackConfig.plugins.push(new BundleAnalyzerPlugin())
}

module.exports = webpackConfig

注释:webpack.prod.conf.js详细内容

五、结语

第一篇博文总在想要写点什么,就根据自己的经验加查了下文档写了这么一篇工具类的文章,由于有些插件有些用法会重复,所以按照文章先后,写过用法或者给过链接的插件,在后面的文章就省略了,有时间的建议从头开始,如果单独看某章节的话遇到不懂的语法或插件可全文查找,也可以点击更多安装包传送门进行查找阅读。本文将vue本身自带的英文注释删除了,但英文注释非常有用可以仔细阅读,希望对大家学习vue和webpack都有所帮助。

尊重原创,如需转载请注明出处!

查看原文

Druidiurd 收藏了文章 · 2018-06-05

详解CommonsChunkPlugin的配置和用法

简介

CommonsChunkPlugin主要是用来提取第三方库和公共模块,避免首屏加载的bundle文件或者按需加载的bundle文件体积过大,从而导致加载时间过长,着实是优化的一把利器。

先来说一下各种教程以及文档中CommonsChunkPlugin提及到chunk有哪几种,主要有以下三种:

  1. webpack当中配置的入口文件(entry)是chunk,可以理解为entry chunk
  2. 入口文件以及它的依赖文件通过code split(代码分割)出来的也是chunk,可以理解为children chunk
  3. 通过CommonsChunkPlugin创建出来的文件也是chunk,可以理解为commons chunk

CommonsChunkPlugin可配置的属性

  • name:可以是已经存在的chunk(一般指入口文件)对应的name,那么就会把公共模块代码合并到这个chunk上;否则,会创建名字为name的commons chunk进行合并
  • filename:指定commons chunk的文件名
  • chunks:指定source chunk,即指定从哪些chunk当中去找公共模块,省略该选项的时候,默认就是entry chunks
  • minChunks:既可以是数字,也可以是函数,还可以是Infinity,具体用法和区别下面会说

children和async属于异步中的应用,放在了最后讲解。

可能这么说,大家会云里雾里,下面用demo来检验上面的属性。

实战应用

以下几个demo主要是测试以下几种情况:

  • 不分离出第三方库和自定义公共模块
  • 分离出第三方库、自定义公共模块、webpack运行文件,但它们在同一个文件中
  • 单独分离第三方库、自定义公共模块、webpack运行文件,各自在不同文件

不分离出第三方库和自定义公共模块

项目初始结构,后面打包后会生成dist目录:

image

src目录下各个文件内容都很简洁的,如下:

common.js
export const common = 'common file';

first.js
import {common} from './common';
import $ from 'jquery';
console.log($,`first  ${common}`);

second.js
import {common} from './common';
import $ from 'jquery';
console.log($,`second ${common}`);

package.json文件:

{
  "name": "test",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "rimraf dist && webpack"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "rimraf": "^2.6.2",
    "webpack": "^3.10.0",
    "webpack-dev-server": "^2.10.1"
  },
  "dependencies": {
    "jquery": "^3.2.1"
  }
}

webpack.config.js:

const path = require("path");
const webpack = require("webpack");

const config = {
    entry: {
        first: './src/first.js',
        second: './src/second.js'
    },
    output: {
        path: path.resolve(__dirname,'./dist'),
        filename: '[name].js'
    },
}

module.exports = config;

接着在命令行npm run build,此时项目中多了dist目录:

image

再来查看一下命令行中webpack的打包信息:

image

查看first.js和second.js,会发现共同引用的common.js文件和jquery都被打包进去了,这肯定不合理,公共模块重复打包,体积过大。

分离出第三方库、自定义公共模块、webpack运行文件

这时候修改webpack.config.js新增一个入口文件vendor和CommonsChunkPlugin插件进行公共模块的提取:

const path = require("path");
const webpack = require("webpack");
const packagejson = require("./package.json");

const config = {
    entry: {
        first: './src/first.js',
        second: './src/second.js',
        vendor: Object.keys(packagejson.dependencies)//获取生产环境依赖的库
    },
    output: {
        path: path.resolve(__dirname,'./dist'),
        filename: '[name].js'
    },
    plugins: [
        new webpack.optimize.CommonsChunkPlugin({
            name: 'vendor',
            filename: '[name].js'
        }),
    ]
}

module.exports = config;

查看dist目录下,新增了一个vendor.js的文件:

image

再来查看一下命令行中webpack的打包信息:

image

通过查看vendor.js文件,发现first.js和second.js文件中依赖的jquery和common.js都被打包进vendor.js中,同时还有webpack的运行文件。总的来说,我们初步的目的达到,提取公共模块,但是它们都在同一个文件中。

到这里,肯定有人希望自家的vendor.js纯白无瑕,只包含第三方库,不包含自定义的公共模块和webpack运行文件,又或者希望包含第三方库和公共模块,不包含webpack运行文件。

其实,这种想法是对,特别是分离出webpack运行文件,因为每次打包webpack运行文件都会变,如果你不分离出webpack运行文件,每次打包生成vendor.js对应的哈希值都会变化,导致vendor.js改变,但实际上你的第三方库其实是没有变,然而浏览器会认为你原来缓存的vendor.js就失效,要重新去服务器中获取,其实只是webpack运行文件变化而已,就要人家重新加载,好冤啊~

OK,接下来就针对这种情况来测试。

单独分离出第三方库、自定义公共模块、webpack运行文件

这里我们分两步走:

  1. 先单独抽离出webpack运行文件
  2. 接着单独抽离第三方库和自定义公共模块,这里利用minChunks有两种方法可以完成,往后看就知道了

1、抽离webpack运行文件

先来抽离webpack运行文件,修改webpack配置文件:

plugins: [
        new webpack.optimize.CommonsChunkPlugin({
            name: ['vendor','runtime'],
            filename: '[name].js'
        }),
    ]

其实上面这段代码,等价于下面这段:

 plugins: [
        new webpack.optimize.CommonsChunkPlugin({
            name: 'vendor',
            filename: '[name].js'
        }),
        new webpack.optimize.CommonsChunkPlugin({
            name: 'runtime',
            filename: '[name].js',
            chunks: ['vendor']
        }),
    ]

上面两段抽离webpack运行文件代码的意思是创建一个名为runtime的commons chunk进行webpack运行文件的抽离,其中source chunks是vendor.js。

查看dist目录下,新增了一个runtime.js的文件,其实就是webpack的运行文件:

image

再来查看一下命令行中webpack的打包信息,你会发现vendor.js的体积已经减小,说明已经把webpack运行文件提取出来了:

image

可是,vendor.js中还有自定义的公共模块common.js,人家只想vendor.js拥有项目依赖的第三方库而已(这里是jquery),这个时候把minChunks这个属性引进来。

minChunks可以设置为数字、函数和Infinity,默认值是2,并不是官方文档说的入口文件的数量,下面解释下minChunks含义:

  • 数字:模块被多少个chunk公共引用才被抽取出来成为commons chunk
  • 函数:接受 (module, count) 两个参数,返回一个布尔值,你可以在函数内进行你规定好的逻辑来决定某个模块是否提取成为commons chunk
  • Infinity:只有当入口文件(entry chunks) >= 3 才生效,用来在第三方库中分离自定义的公共模块

2、抽离第三方库和自定义公共模块

要在vendor.js中把第三方库单独抽离出来,上面也说到了有两种方法。

第一种方法minChunks设为Infinity,修改webpack配置文件如下:

plugins: [
        new webpack.optimize.CommonsChunkPlugin({
            name: ['vendor','runtime'],
            filename: '[name].js',
            minChunks: Infinity
        }),
        new webpack.optimize.CommonsChunkPlugin({
            name: 'common',
            filename: '[name].js',
            chunks: ['first','second']//从first.js和second.js中抽取commons chunk
        }),
    ]

查看dist目录下,新增了一个common.js的文件:

image

再来查看一下命令行中webpack的打包信息,自定义的公共模块分离出来:

image

这时候的vendor.js就纯白无瑕,只包含第三方库文件,common.js就是自定义的公共模块,runtime.js就是webpack的运行文件。

第二种方法把它们分离开来,就是利用minChunks作为函数的时候,说一下minChunks作为函数两个参数的含义:

  • module:当前chunk及其包含的模块
  • count:当前chunk及其包含的模块被引用的次数

minChunks作为函数会遍历每一个入口文件及其依赖的模块,返回一个布尔值,为true代表当前正在处理的文件(module.resource)合并到commons chunk中,为false则不合并。

继续修改我们的webpack配置文件,把vendor入口文件注释掉,用minChunks作为函数实现vendor只包含第三方库,达到和上面一样的效果:

const config = {
    entry: {
        first: './src/first.js',
        second: './src/second.js',
        //vendor: Object.keys(packagejson.dependencies)//获取生产环境依赖的库
    },
    output: {
        path: path.resolve(__dirname,'./dist'),
        filename: '[name].js'
    },
     plugins: [
        new webpack.optimize.CommonsChunkPlugin({
            name: 'vendor',
            filename: '[name].js',
            minChunks: function (module,count) {
                console.log(module.resource,`引用次数${count}`);
                //"有正在处理文件" + "这个文件是 .js 后缀" + "这个文件是在 node_modules 中"
                return (
                    module.resource &&
                    /\.js$/.test(module.resource) &&
                    module.resource.indexOf(path.join(__dirname, './node_modules')) === 0
                )
            }
        }),
        new webpack.optimize.CommonsChunkPlugin({
            name: 'runtime',
            filename: '[name].js',
            chunks: ['vendor']
        }),
    ]
}

上面的代码其实就是生成一个叫做vendor的commons chunk,那么有哪些模块会被加入到vendor中呢?就对入口文件及其依赖的模块进行遍历,如果该模块是js文件并且在node_modules中,就会加入到vendor当中,其实这也是一种让vendor只保留第三方库的办法。

再来查看一下命令行中webpack的打包信息:

image

你会发现,和上面minChunks设为Infinity的结果是一致的。

children和async属性

这两个属性主要是在code split(代码分割)和异步加载当中应用。

  • children

    • 指定为true的时候,就代表source chunks是通过entry chunks(入口文件)进行code split出来的children chunks
    • children和chunks不能同时设置,因为它们都是指定source chunks的
    • children 可以用来把 entry chunk 创建的 children chunks 的共用模块合并到自身,但这会导致初始加载时间较长
  • async:即解决children:true时合并到entry chunks自身时初始加载时间过长的问题。async设为true时,commons chunk 将不会合并到自身,而是使用一个新的异步的commons chunk。当这个children chunk 被下载时,自动并行下载该commons chunk

修改webpack配置文件,增加chunkFilename,如下:

 output: {
        ...........
        chunkFilename: "[name].[hash:5].chunk.js",
    },
plugins: [
    new webpack.optimize.CommonsChunkPlugin({
        name: ['vendor','runtime'],
        filename: '[name].js',
        minChunks: Infinity
    }),
   new webpack.optimize.CommonsChunkPlugin({
        children: true,
        async: 'children-async'
    })
]

chunkFilename用来指定异步加载的模块名字,异步加载模块中的共同引用到的模块就会被合并到async中指定名字,上面就是children-async。

修改成异步截图出来太麻烦了,就简单说明一下:first和second是异步加载模块,同时它们共同引用了common.js这个模块,如果你不设置这一步:

 new webpack.optimize.CommonsChunkPlugin({
        children: true,
        async: 'children-async'
    })

那么共同引用的common.js都被打包进各自的模块当中,就重复打包了。

OK,你设置之后,也得看children的脸色怎么来划分:

  • children为true,共同引用的模块就会被打包合并到名为children-async的公共模块,当你懒加载first或者second的时候并行加载这和children-async公共模块
  • children为false,共同引用的模块就会被打包到首屏加载的app.bundle当中,这就会导致首屏加载过长了,而且也不要用到,所以最好还是设为true

浏览器缓存的实现

先来说一下哈希值的不同:

  • hash 是 build-specific ,即每次编译都不同——适用于开发阶段
  • chunkhash 是 chunk-specific,是根据每个 chunk 的内容计算出的 hash——适用于生产

所以,在生产环境,要把文件名改成'[name].[chunkhash]',最大限度的利用浏览器缓存。

最后,写这篇文章,自己测试了很多demo,当然不可能全部贴上,但还是希望自己多动手测试以下,真的坑中带坑。

也参考了很多文章:

查看原文

Druidiurd 提出了问题 · 2018-06-02

解决Javascript 正则表达式惰性模式的一个问题?

正则表达式惰性模式,表示如果表达式能匹配成功前提下,会尽可能的少匹配字符。
根据字面意思理解,看我如下代码

var pattern=/a(\w*?)/;  
var str='a123a';  
console.log(str.replace(pattern,'$1'));

我预期输出的是 '1',因为惰性模式,匹配一个字符就可以使之匹配成功
可实际输出的是 '123a',与贪婪模式输出一摸一样

这是为什么?

关注 3 回答 2

认证与成就

  • 获得 17 次点赞
  • 获得 9 枚徽章 获得 0 枚金徽章, 获得 2 枚银徽章, 获得 7 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-01-30
个人主页被 462 人浏览