liuy666

liuy666 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

liuy666 收藏了文章 · 9月27日

深入理解 ESLint

前言

小沈是一个刚刚开始工作的前端实习生,第一次进行团队开发,难免有些紧张。在导师的安排下,拿到了项目的 git 权限,开始进行 clone。

$ git clone git@github.com:company/project.git

小沈开始细细品味着同事们的代码,终于在他的不懈努力下,发现了老王 2 年前写的一个 bug,跟导师报备之后,小沈开始着手修改。年轻人嘛,容易冲动,不仅修复了老王的 bug,还把这部分代码进行了重构,使用了前两天刚刚从书里学会的策略模式,去掉了一些不必要 if else 逻辑。小沈潇洒的摸了摸自己稀疏的头发,得意的准备提交代码,想着第一天刚来就秀了下自己的超强的编码能力。接下来可怕的事情发生了,代码死活不能通过 lint 工具的检测,急得他面红耳赤,赶紧跑去问导师,导师告诉他,只要按照控制台的 warning 修改代码就好。小沈反驳道,这个 lint 工具非让我去掉分号,我在学校的时候,老师就教我分号是必不可少的,没有分号的代码是不完美的。导师无奈的笑了笑,打开了小沈的实习评分表,在团队合作一项中勾选『较差』。

不服气的小沈,写了一篇博客发布到了 CSDN 上,还收获了不少阅读量。

image

问:工作第一天小沈犯了哪些错误?

  1. 对不了解的业务代码进行重构,这是业务开发的大忌;
  2. 没有遵守团队规范,团队开发带有太强的个人情绪;
  3. 上面都是我编的,听说现在写文章开头都要编个故事。

lint 工具简史

在计算机科学中,lint是一种工具的名称,它用来标记代码中,某些可疑的、不具结构性(可能造成bug)的语句。它是一种静态程序分析工具,最早适用于C语言,在UNIX平台上开发出来。后来它成为通用术语,可用于描述在任何一种编程语言中,用来标记代码中有疑义语句的工具。 -- by wikipedia

在 JavaScript 20 多年的发展历程中,也出现过许许多多的 lint 工具,下面就来介绍下主流的三款 lint 工具。

  1. JSLint
  2. JSHint
  3. ESLint

image

JSLint

JSLint logo

JSLint 可以说是最早出现的 JavaScript 的 lint 工具,由 Douglas Crockford (《JavaScript 语言精粹》作者) 开发。从《JavaScript 语言精粹》的笔风就能看出,Douglas 是个眼里容不得瑕疵的人,所以 JSLint 也继承了这个特色,JSLint 的所有规则都是由 Douglas 自己定义的,可以说这是一个极具 Douglas 个人风格的 lint 工具,如果你要使用它,就必须接受它所有规则。值得称赞的是,JSLint 依然在更新,而且也提供了 node 版本:node-jslint

JSHint

JSHint logo

由于 JSLint 让很多人无法忍受它的规则,感觉受到了压迫,所以 Anton Kovalyov (现在在 Medium 工作) 基于 JSLint 开发了 JSHint。JSHint 在 JSLint 的基础上提供了丰富的配置项,给了开发者极大的自由,JSHint 一开始就保持着开源软件的风格,由社区进行驱动,发展十分迅速。早起 jQuery 也是使用 JSHint 进行代码检查的,不过现在已经转移到 ESLint 了。

ESLint

ESLint logo

ESLint 由 Nicholas C. Zakas (《JavaScript 高级程序设计》作者) 于2013年6月创建,它的出现因为 Zakas 想使用 JSHint 添加一条自定义的规则,但是发现 JSHint 不支持,于是自己开发了一个。

ESLint 号称下一代的 JS Linter 工具,它的灵感来源于 PHP Linter,将源代码解析成 AST,然后检测 AST 来判断代码是否符合规则。ESLint 使用 esprima 将源代码解析吃成 AST,然后你就可以使用任意规则来检测 AST 是否符合预期,这也是 ESLint 高可扩展性的原因。

早期源码

var ast = esprima.parse(text, { loc: true, range: true }),
    walk = astw(ast);

walk(function(node) {
    api.emit(node.type, node);
});

return messages;

但是,那个时候 ESLint 并没有大火,因为需要将源代码转成 AST,运行速度上输给了 JSHint ,并且 JSHint 当时已经有完善的生态(编辑器的支持)。真正让 ESLint 大火是因为 ES6 的出现。

ES6 发布后,因为新增了很多语法,JSHint 短期内无法提供支持,而 ESLint 只需要有合适的解析器就能够进行 lint 检查。这时 babel 为 ESLint 提供了支持,开发了 babel-eslint,让ESLint 成为最快支持 ES6 语法的 lint 工具。

谷歌趋势

在 2016 年,ESLint整合了与它同时诞生的另一个 lint 工具:JSCS,因为它与 ESLint 具有异曲同工之妙,都是通过生成 AST 的方式进行规则检测。

ESLint整合JSCS

自此,ESLint 在 JS Linter 领域一统江湖,成为前端界的主流工具。

Lint 工具的意义

下面一起来思考一个问题:Lint 工具对工程师来说到底是代码质量的保证还是一种束缚?

然后,我们再看看 ESLint 官网的简介:

代码检查是一种静态的分析,常用于寻找有问题的模式或者代码,并且不依赖于具体的编码风格。对大多数编程语言来说都会有代码检查,一般来说编译程序会内置检查工具。

JavaScript 是一个动态的弱类型语言,在开发中比较容易出错。因为没有编译程序,为了寻找 JavaScript 代码错误通常需要在执行过程中不断调试。像 ESLint 这样的可以让程序员在编码的过程中发现问题而不是在执行的过程中。

因为 JavaScript 这门神奇的语言,在带给我们灵活性的同时,也埋下了一些坑。比如 == 涉及到的弱类型转换,着实让人很苦恼,还有 this 的指向,也是一个让人迷惑的东西。而 Lint 工具就很好的解决了这个问题,干脆禁止你使用 == ,这种做法虽然限制了语言的灵活性,但是带来的收益也是可观的。

还有就是作为一门动态语言,因为缺少编译过程,有些本可以在编译过程中发现的错误,只能等到运行才发现,这给我们调试工作增加了一些负担,而 Lint 工具相当于为语言增加了编译过程,在代码运行前进行静态分析找到出错的地方。

所以汇总一下,Lint工具的优势:

1. 避免低级bug,找出可能发生的语法错误

使用未声明变量、修改 const 变量……

2. 提示删除多余的代码

声明而未使用的变量、重复的 case ……

3. 确保代码遵循最佳实践

可参考 airbnb stylejavascript standard

4. 统一团队的代码风格

加不加分号?使用 tab 还是空格?

使用方式

说了那么多,还是来看下有点实际意义的,ESLint 到底是如何使用的。

初始化

如果想在现有项目中引入 ESLint,可以直接运行下面的命令:

# 全局安装 ESLint
$ npm install -g eslint

# 进入项目
$ cd ~/Code/ESLint-demo

# 初始化 package.json
$ npm init -f

# 初始化 ESLint 配置
$ eslint --init

image

在使用 eslint --init 后,会出现很多用户配置项,具体可以参考:eslint cli 部分的源码

经过一系列一问一答的环节后,你会发现在你文件夹的根目录生成了一个 .eslintrc.js 文件。

image

配置方式

ESLint 一共有两种配置方式:

1. 使用注释把 lint 规则直接嵌入到源代码中

这是最简单粗暴的方式,直接在源代码中使用 ESLint 能够识别的注释方式,进行 lint 规则的定义。

/* eslint eqeqeq: "error" */
var num = 1
num == '1'

image

当然我们一般使用注释是为了临时禁止某些严格的 lint 规则出现的警告:

/* eslint-disable */
alert('该注释放在文件顶部,整个文件都不会出现 lint 警告')

/* eslint-enable */
alert('重新启用 lint 告警')

/* eslint-disable eqeqeq */
alert('只禁止某一个或多个规则')

/* eslint-disable-next-line */
alert('当前行禁止 lint 警告')

alert('当前行禁止 lint 警告') // eslint-disable-line

2. 使用配置文件进行 lint 规则配置

在初始化过程中,有一个选项就是使用什么文件类型进行 lint 配置(What format do you want your config file to be in?):

{
    type: "list",
    name: "format",
    message: "What format do you want your config file to be in?",
    default: "JavaScript",
    choices: ["JavaScript", "YAML", "JSON"]
}

官方一共提供了三个选项:

  1. JavaScript (eslintrc.js)
  2. YAML (eslintrc.yaml)
  3. JSON (eslintrc.json)

另外,你也可以自己在 package.json 文件中添加 eslintConfig 字段进行配置。

翻阅 ESLint 源码可以看到,其配置文件的优先级如下:

const configFilenames = [
  ".eslintrc.js",
  ".eslintrc.yaml",
  ".eslintrc.yml",
  ".eslintrc.json",
  ".eslintrc",
  "package.json"
];
.eslintrc.js > .eslintrc.yaml  > .eslintrc.yml > .eslintrc.json > .eslintrc > package.json

当然你也可以使用 cli 自己指定配置文件路径:

image

项目级与目录级的配置

我们有如下目录结构,此时在根目录运行 ESLint,那么我们将得到两个配置文件 .eslintrc.js(项目级配置) 和 src/.eslintrc.js(目录级配置),这两个配置文件会进行合并,但是 src/.eslintrc.js 具有更高的优先级。

目录结构

但是,我们只要在 src/.eslintrc.js 中配置 "root": true,那么 ESLint 就会认为 src 目录为根目录,不再向上查找配置。

{
  "root": true
}

配置参数

下面我们一起来细细品味 ESLinte 的配置规则。

解析器配置

{
  // 解析器类型
  // espima(默认), babel-eslint, @typescript-eslint/parse
  "parse": "esprima",
  // 解析器配置参数
  "parseOptions": {
    // 代码类型:script(默认), module
    "sourceType": "script",
    // es 版本号,默认为 5,也可以是用年份,比如 2015 (同 6)
    "ecamVersion": 6,
    // es 特性配置
    "ecmaFeatures": {
        "globalReturn": true, // 允许在全局作用域下使用 return 语句
        "impliedStrict": true, // 启用全局 strict mode 
        "jsx": true // 启用 JSX
    },
  }
}

对于 @typescript-eslint/parse 这个解析器,主要是为了替代之前存在的 TSLint,TS 团队因为 ESLint 生态的繁荣,且 ESLint 具有更多的配置项,不得不抛弃 TSLint 转而实现一个 ESLint 的解析器。同时,该解析器拥有不同的配置

{
  "parserOptions": {
    "ecmaFeatures": {
      "jsx": true
    },
    "useJSXTextNode": true,
    "project": "./tsconfig.json",
    "tsconfigRootDir": "../../",
    "extraFileExtensions": [".vue"]
  }
}

环境与全局变量

ESLint 会检测未声明的变量,并发出警告,但是有些变量是我们引入的库声明的,这里就需要提前在配置中声明。

{
  "globals": {
    // 声明 jQuery 对象为全局变量
    "$": false // true表示该变量为 writeable,而 false 表示 readonly
  }
}

globals 中一个个的进行声明未免有点繁琐,这个时候就需要使用到 env ,这是对一个环境定义的一组全局变量的预设(类似于 babel 的 presets)。

{
  "env": {
    "amd": true,
    "commonjs": true,
    "jquery": true
  }
}

可选的环境很多,预设值都在这个文件中进行定义,查看源码可以发现,其预设变量都引用自 globals 包。

env

env

规则设置

ESLint 附带有大量的规则,你可以在配置文件的 rules 属性中配置你想要的规则。每一条规则接受一个参数,参数的值如下:

  • "off" 或 0:关闭规则
  • "warn" 或 1:开启规则,warn 级别的错误 (不会导致程序退出)
  • "error" 或 2:开启规则,error级别的错误(当被触发的时候,程序会退出)

举个例子,我们先写一段使用了平等(equality)的代码,然后对 eqeqeq 规则分别进行不同的配置。

// demo.js
var num = 1
num == '1'

eqeqeq 规则校验

这里使用了命令行的配置方式,如果你只想对单个文件进行某个规则的校验就可以使用这种方式。

但是,事情往往没有我们想象中那么简单,ESLint 的规则不仅只有关闭和开启这么简单,每一条规则还有自己的配置项。如果需要对某个规则进行配置,就需要使用数组形式的参数。

我们看下 quotes 规则,根据官网介绍,它支持字符串和对象两个配置项。

quotes

{
  "rules": {
    // 使用数组形式,对规则进行配置
    // 第一个参数为是否启用规则
    // 后面的参数才是规则的配置项
    "quotes": [
      "error",
      "single",
      {
        "avoidEscape": true 
      }
    ]
  }
}

根据上面的规则:

// bad
var str = "test 'ESLint' rule"

// good
var str = 'test "ESLint" rule'

扩展

扩展就是直接使用别人已经写好的 lint 规则,方便快捷。扩展一般支持三种类型:

{
  "extends": [
    "eslint:recommended",
    "plugin:react/recommended",
    "eslint-config-standard",
  ]
}
  • eslint: 开头的是 ESLint 官方的扩展,一共有两个:eslint:recommendedeslint:all
  • plugin: 开头的是扩展是插件类型,也可以直接在 plugins 属性中进行设置,后面一节会详细讲到。
  • 最后一种扩展来自 npm 包,官方规定 npm 包的扩展必须以 eslint-config- 开头,使用时可以省略这个头,上面案例中 eslint-config-standard 可以直接简写成 standard

如果你觉得自己的配置十分满意,也可以将自己的 lint 配置发布到 npm 包,只要将包名命名为 eslint-config-xxx 即可,同时,需要在 package.json 的 peerDependencies 字段中声明你依赖的 ESLint 的版本号。

插件

使用插件

虽然官方提供了上百种的规则可供选择,但是这还不够,因为官方的规则只能检查标准的 JavaScript 语法,如果你写的是 JSX 或者 Vue 单文件组件,ESLint 的规则就开始束手无策了。

这个时候就需要安装 ESLint 的插件,来定制一些特定的规则进行检查。ESLint 的插件与扩展一样有固定的命名格式,以 eslint-plugin- 开头,使用的时候也可以省略这个头。

npm install --save-dev eslint-plugin-vue eslint-plugin-react
{
  "plugins": [
    "react", // eslint-plugin-react
    "vue",   // eslint-plugin-vue
  ]
}

或者是在扩展中引入插件,前面有提到 plugin: 开头的是扩展是进行插件的加载。

{
  "extends": [
    "plugin:react/recommended",
  ]
}

通过扩展的方式加载插件的规则如下:

extPlugin = `plugin:${pluginName}/${configName}`

对照上面的案例,插件名(pluginName) 为 react,也就是之前安装 eslint-plugin-react 包,配置名(configName)为 recommended。那么这个配置名又是从哪里来的呢?

可以看到 eslint-plugin-react源码

module.exports = {
  // 自定义的 rule
  rules: allRules,
  // 可用的扩展
  configs: {
    // plugin:react/recommended
    recomended: {
      plugins: [ 'react' ]
      rules: {...}
    },
    // plugin:react/all
    all: {
      plugins: [ 'react' ]
      rules: {...}
    }
  }
}

配置名是插件配置的 configs 属性定义的,这里的配置其实就是 ESLint 的扩展,通过这种方式即可以加载插件,又可以加载扩展。

开发插件

ESLint 官方为了方便开发者,提供了 Yeoman 的模板(generator-eslint)。

# 安装模块
npm install -g yo generator-eslint

# 创建目录
mkdir eslint-plugin-demo
cd eslint-plugin-demo

# 创建模板
yo eslint:plugin

eslint:plugin

eslint:plugin 目录

创建好项目之后,就可以开始创建一条规则了,幸运的是 generator-eslint 除了能够生成插件的模板代码外,还具有创建规则的模板代码。打开之前创建的 eslint-plugin-demo 文件夹,在该目录下添加一条规则,我希望这条规则能检测出我的代码里面是否有 console ,所以,我给该规则命名为 disable-console

yo eslint:rule

eslint:rule

eslint:rule 目录

接下来我们看看如何来指定 ESLinte 的一个规则:

打开 lib/rules/disable-console.js ,可以看到默认的模板代码如下:

module.exports = {
  meta: {
    docs: {
      description: "disable console",
      category: "Fill me in",
      recommended: false
    },
    schema: []
  },
  create: function (context) {
    // variables should be defined here
    return {
      // give me methods
    };
  }
};

简单的介绍下其中的参数(更详细的介绍可以查看官方文档):

  • meta:规则的一些描述信息

    • docs:规则的描述对象

      • descrition(string):规则的简短描述
      • category(string): 规则的类别(具体类别可以查看官网
      • recommended(boolean):是否加入 eslint:recommended
    • schema(array):规则所接受的配置项
  • create:返回一个对象,该对象包含 ESLint 在遍历 JavaScript 代码 AST 时,所触发的一系列事件勾子。

在详细讲解如何创建一个规则之前,我们先来谈谈 AST(抽象语法树)。ESLint 使用了一个叫做 Espree 的 JavaScript 解析器来把 JavaScript 代码解析为一个 AST 。然后深度遍历 AST,每条规则都会对匹配的过程进行监听,每当匹配到一个类型,相应的规则就会进行检查。为了方便查看 AST 的各个节点类型,这里提供一个网站能十分清晰的查看一段代码解析成 AST 之后的样子:astexplorer。如果你想找到所有 AST 节点的类型,可以查看 estree

astexplorer

astexplorer

可以看到 console.log() 属于 ExpressionStatement(表达式语句) 中的 CallExpression(调用语句)

{
  "type": "ExpressionStatement",
  "expression": {
    "type": "CallExpression",
    "callee": {
      "type": "MemberExpression",
      "object": {
        "type": "Identifier",
        "name": "console"
      },
      "property": {
        "type": "Identifier",
        "name": "log"
      },
      "computed": false
    }
  }
}

所以,我们要判断代码中是否调用了 console,可以在 create 方法返回的对象中,写一个 CallExpression 方法,在 ESLint 遍历 AST 的过程中,对调用语句进行监听,然后检查该调用语句是否为 console 调用。

module.exports = {
  create: function(context) {
    return {
      CallExpression(node) {
        // 获取调用语句的调用对象
        const callObj = node.callee.object
        if (!callObj) {
          return
        }
        if (callObj.name === 'console') {
          // 如果调用对象为 console,通知 ESLint
          context.report({
            node,
            message: 'error: should remove console'
          })
        }
      },
    }
  }
}

可以看到我们最后通过 context.report 方法,告诉 ESLint 这是一段有问题的代码,具体要怎么处理,就要看 ESLint 配置中,该条规则是 [off, warn, error] 中的哪一个了。

之前介绍规则的时候,有讲到规则是可以接受配置的,下面看看我们自己制定规则的时候,要如何接受配置项。其实很简单,只需要在 mate 对象的 schema 中定义好参数的类型,然后在 create 方法中,通过 context.options 获取即可。下面对 disable-console 进行修改,毕竟禁止所有的 console 太过严格,我们可以添加一个参数,该参数是一个数组,表示允许调用的 console 方法。

module.exports = {
  meta: {
    docs: {
      description: "disable console", // 规则描述
      category: "Possible Errors",    // 规则类别
      recommended: false
    },
    schema: [ // 接受一个参数
      {
        type: 'array', // 接受参数类型为数组
        items: {
          type: 'string' // 数组的每一项为一个字符串
        }
      }
    ]
  },

  create: function(context) {
    const logs = [ // console 的所以方法
        "debug", "error", "info", "log", "warn", 
        "dir", "dirxml", "table", "trace", 
        "group", "groupCollapsed", "groupEnd", 
        "clear", "count", "countReset", "assert", 
        "profile", "profileEnd", 
        "time", "timeLog", "timeEnd", "timeStamp", 
        "context", "memory"
    ]
    return {
      CallExpression(node) {
         // 接受的参数
        const allowLogs = context.options[0]
        const disableLogs = Array.isArray(allowLogs)
          // 过滤掉允许调用的方法
          ? logs.filter(log => !allowLogs.includes(log))
          : logs
        const callObj = node.callee.object
        const callProp = node.callee.property
        if (!callObj || !callProp) {
          return
        }
        if (callObj.name !== 'console') {
          return
        }
        // 检测掉不允许调用的 console 方法
        if (disableLogs.includes(callProp.name)) {
          context.report({
            node,
            message: 'error: should remove console'
          })
        }
      },
    }
  }
}

规则写完之后,打开 tests/rules/disable-console.js ,编写测试用例。

var rule = require("../../../lib/rules/disable-console")
var RuleTester = require("eslint").RuleTester

var ruleTester = new RuleTester()
ruleTester.run("disable-console", rule, {
  valid: [{
    code: "console.info(test)",
    options: [['info']]
  }],
  invalid: [{
    code: "console.log(test)",
    errors: [{ message: "error: should remove console" }]
  }]
});

test

最后,只需要引入插件,然后开启规则即可。

// eslintrc.js
module.exports = {
  plugins: [ 'demo' ],
  rules: {
    'demo/disable-console': [
      'error', [ 'info' ]
    ],
  }
}

use plugin demo

最佳配置

最佳配置

业界有许多 JavaScript 的推荐编码规范,较为出名的就是下面两个:

  1. airbnb style
  2. javascript standard

同时这里也推荐 AlloyTeam 的 eslint-config-alloy

但是代码规范这个东西,最好是团队成员之间一起来制定,确保大家都能够接受,如果实在是有人有异议,就只能少数服从多数了。虽然这节的标题叫最佳配置,但是软件行业并有没有什么方案是最佳方案,即使 javascript standard 也不是 javascript 标准的编码规范,它仅仅只是叫这个名字而已,只有适合的才是最好的。

最后安利一下,将 ESLint 和 Prettier 结合使用,不仅统一编码规范,也能统一代码风格。具体实践方式,请参考我的文章:使用ESLint+Prettier来统一前端代码风格

总结

看到这里我们做一个总结,JavaScript 的 linter 工具发展历史其实也不算短,ESLint 之所以能够后来者居上,主要原因还是 JSLint 和 JSHint 采用自顶向下的方式来解析代码,并且早期 JavaScript 语法万年不更新,能这种方式够以较快的速度来解析代码,找到可能存在的语法错误和不规范的代码。但是 ES6 发布之后,JavaScript 语法发生了很多的改动,比如:箭头函数、模板字符串、扩展运算符……,这些语法的发布,导致 JSLint 和 JSHint 如果不更新解析器就没法检测 ES6 的代码。而 ESLint 另辟蹊径,采用 AST 的方式对代码进行静态分析,并保留了强大的可扩展性和灵活的配置能力。这也告诉我们,在日常的编码过程中,一定要考虑到后续的扩展能力。

正是因为这个强大扩展能力,让业界的很多 JavaScript 编码规范能够在各个团队进行快速的落地,并且团队自己定制的代码规范也可以对外共享。

最后,希望你通过上面的学习已经理解了 ESLint 带来的好处,同时掌握了 ESLint 的用法,并可以为现有的项目引入 ESLint 改善项目的代码质量。

参考

image

查看原文

liuy666 收藏了文章 · 9月24日

没有了CommonsChunkPlugin,咱拿什么来分包(译)

原文:RIP CommonsChunkPlugin

webpack 4 移除 CommonsChunkPlugin,取而代之的是两个新的配置项(optimization.splitChunks 和 optimization.runtimeChunk),下面介绍一下用法和机制。

默认方式

webpack模式模式现在已经做了一些通用性优化,适用于多数使用者。

需要注意的是:默认模式只影响按需(on-demand)加载的代码块(chunk),因为改变初始代码块会影响声明在HTML的script标签。如果可以处理好这些(比如,从打包状态里面读取并动态生成script标签到HTML),你可以通过设置optimization.splitChunks.chunks: "all",应用这些优化模式到初始代码块(initial chunk)。

webpack根据下述条件自动进行代码块分割:

  • 新代码块可以被共享引用,OR这些模块都是来自node_modules文件夹里面
  • 新代码块大于30kb(min+gziped之前的体积)
  • 按需加载的代码块,最大数量应该小于或者等于5
  • 初始加载的代码块,最大数量应该小于或等于3

为了满足后面两个条件,webpack有可能受限于包的最大数量值,生成的代码体积往上增加。

我们来看一下一些例子:

Example 1

// entry.js
import("./a");
// a.js
import "react-dom";
// ...

结果:webpack会创建一个包含react-dom的分离代码块。当import调用时候,这个代码块就会与./a代码被并行加载。

为什么会这样打包:

  • 条件1:这个代码块是从node_modules来的
  • 条件2:react-dom大于30kb
  • 条件3:按需请求的数量是2(小于5)
  • 条件4:不会影响初始代码请求数量

这样打包有什么好处呢?

对比起你的应用代码,react-dom可能不会经常变动。通过将它分割至另外一个代码块,这个代码块可以被独立缓存起来(假设你在用的是长期缓存策略:chunkhash,records,Cache-Control)

Example 2

// entry.js
import("./a");
import("./b");
// a.js
import "./helpers"; // helpers is 40kb in size
// ...
// b.js
import "./helpers";
import "./more-helpers"; // more-helpers is also 40kb in size
// ...

结果:webpack会创建一个包含./helpers的独立代码块,其他模块会依赖于它。在import被调用时候,这个代码块会跟原始的代码并行加载(译注:它会跟a.jsb.js并行加载)。

为什么会这样打包:

  • 条件1:这个代码块会被两个导入(import)调用依赖(指的是a.jsb.js
  • 条件2:helpers体积大于30kb
  • 条件3:按需请求的数量是2(小于5)
  • 条件4:不会影响初始代码请求数量

这样打包有什么好处呢?

helpers代码放在每一个依赖的块里,可能就意味着,用户重复会下载它两次。通过用一个独立的代码块分割,它只需要被下载一次。实际上,这只是一种折衷方案,因为我们为此需要付出额外的一次请求的代价。这就是为什么默认webpack将最小代码块分割体积设置成30kb(译注:太小体积的代码块被分割,可能还会因为额外的请求,拖慢加载性能)。

通过optimizations.splitChunks.chunks: "all",上面的策略也可以应用到初始代码块上(inital chunks)。代码代码块也会被多个入口共享&按需加载(译注:以往我们使用CommonsChunkPlugin最通常的目的)。

配置

如果想要更深入控制这个按需分块的功能,这里提供很多选项来满足你的需求。

Disclaimer:不要在没有实践测量的情况下,尝试手动优化这些参数。默认模式是经过千挑万选的,可以用于满足最佳web性能的策略。

缓存组(Cache Group)

这项优化可以用于将模块分配到对应的Cache group

默认模式会将所有来自node_modules的模块分配到一个叫vendors的缓存组;所有重复引用至少两次的代码,会被分配到default的缓存组。

一个模块可以被分配到多个缓存组,优化策略会将模块分配至跟高优先级别(priority)的缓存组,或者会分配至可以形成更大体积代码块的组里。

Conditions

在满足下述所有条件时,那些从相同代码块和缓存组来的模块,会形成一个新的代码块(译注:比如,在满足条件下,一个vendoer可能会被分割成两个,以充分利用并行请求性能)。

有四个选项可以用于配置这些条件:

  • minSize(默认是30000):形成一个新代码块最小的体积
  • minChunks(默认是1):在分割之前,这个代码块最小应该被引用的次数(译注:保证代码块复用性,默认配置的策略是不需要多次引用也可以被分割)
  • maxInitialRequests(默认是3):一个入口最大的并行请求数
  • maxAsyncRequests(默认是5):按需加载时候最大的并行请求数。

Naming

要控制代码块的命名,可以用name参数来配置。

注意:当不同分割代码块被赋予相同名称时候,他们会被合并在一起。这个可以用于在:比如将那些多个入口/分割点的共享模块(vendor)合并在一起,不过不推荐这样做。这可能会导致加载额外的代码。

如果赋予一个神奇的值true,webpack会基于代码块和缓存组的key自动选择一个名称。除此之外,可以使用字符串或者函数作为参数值。

当一个名称匹配到相应的入口名称,这个入口会被移除。

Select chunks

通过chunks选项,可以配置控制webpack选择哪些代码块用于分割(译注:其他类型代码块按默认方式打包)。有3个可选的值:initialasyncall。webpack将会只对配置所对应的代码块应用这些策略。

reuseExistingChunk选项允许复用已经存在的代码块,而不是新建一个新的,需要在精确匹配到对应模块时候才会生效。

这个选项可以在每个缓存组(Cache Group)里面做配置。

Select modules

test选项用于控制哪些模块被这个缓存组匹配到。原封不动传递出去的话,它默认会选择所有的模块。可以传递的值类型:RegExpStringFunction

通过这个选项,可以通过绝对资源路径(absolute modules resource path)或者代码块名称(chunk names)来匹配对应模块。当一个代码块名称(chunk name)被匹配到,这个代码块的所有模块都会被选中。

配置缓存组(Configurate cache group)

这是默认的配置:

splitChunks: {
    chunks: "async",
    minSize: 30000,
    minChunks: 1,
    maxAsyncRequests: 5,
    maxInitialRequests: 3,
    name: true,
    cacheGroups: {
        default: {
            minChunks: 2,
            priority: -20,
            reuseExistingChunk: true,
        },
        vendors: {
            test: /[\\/]node_modules[\\/]/,
            priority: -10
        }
    }
}

默认来说,缓存组会继承splitChunks的配置,但是testpriortyreuseExistingChunk只能用于配置缓存组。

cacheGroups是一个对象,按上述介绍的键值对方式来配置即可,值代表对应的选项:

除此之外,所有上面列出的选择都是可以用在缓存组里的:chunks, minSize, minChunks, maxAsyncRequests, maxInitialRequests, name

可以通过optimization.splitChunks.cacheGroups.default: false禁用default缓存组。

default缓存组的优先级(priotity)是负数,因此所有自定义缓存组都可以有比它更高优先级(译注:更高优先级的缓存组可以优先打包所选择的模块)(默认自定义缓存组优先级为0)

可以用一些例子来说明:

Example 1

splitChunks: {
    cacheGroups: {
        commons: {
            name: "commons",
            chunks: "initial",
            minChunks: 2
        }
    }
}

这会创建一个commons代码块,这个代码块包含所有被其他入口(entrypoints)共享的代码。

注意:这可能会导致下载额外的代码。

Example 2

splitChunks: {
    cacheGroups: {
        commons: {
            test: /[\\/]node_modules[\\/]/,
            name: "vendors",
            chunks: "all"
        }
    }
}

这会创建一个名为vendors的代码块,它会包含整个应用所有来自node_modules的代码。

注意:这可能会导致下载额外的代码。

optimization.runtimeChunk

通过optimization.runtimeChunk: true选项,webpack会添加一个只包含运行时(runtime)额外代码块到每一个入口。(译注:这个需要看场景使用,会导致每个入口都加载多一份运行时代码)

查看原文

liuy666 收藏了文章 · 9月24日

前端项目修改默认滚动条样式

前端项目修改默认滚动条样式

写过挺多项目都需要改变滚动条的默认样式 并不想单独下载和引入插件 因此纯css修改默认滚动条的样式 这次算统一整理下方法,直接上代码。

            &::-webkit-scrollbar {
              // 滚动条的背景
              width: 16px;
              background: #191a37;
              height: 14px;
            }

            &::-webkit-scrollbar-track,
            &::-webkit-scrollbar-thumb {
              border-radius: 999px;
              width: 20px;
              border: 5px solid transparent;
            }

            &::-webkit-scrollbar-track {
              box-shadow: 1px 1px 5px #191a37 inset;
            }

            &::-webkit-scrollbar-thumb {
              //滚动条的滑块样式修改
              width: 20px;
              min-height: 20px;
              background-clip: content-box;
              box-shadow: 0 0 0 5px #464f70 inset;
            }

            &::-webkit-scrollbar-corner {
              background: #191a37;
            }

这个算很齐全的修改

下边这个很精简。值得一试

            &::-webkit-scrollbar {
              width: 6px;
              height: 6px;
              background: transparent;
            }

            &::-webkit-scrollbar-thumb {
              background: transparent;
              border-radius: 4px;
            }

            &:hover::-webkit-scrollbar-thumb {
              background: hsla(0, 0%, 53%, 0.4);
            }

            &:hover::-webkit-scrollbar-track {
              background: hsla(0, 0%, 53%, 0.1);
            }

这个的优点在于 鼠标移上才会显示修改的滚动条 体验很好

(提示下 隐藏某轴的滚动条代码写法)

overflow-x:hidden;

之前我以为是 none,半天没变化

查看原文

liuy666 收藏了文章 · 9月24日

JS中的垃圾回收与内存泄漏

1. 介绍

浏览器的 Javascript 具有自动垃圾回收机制(GC:Garbage Collecation),也就是说,执行环境会负责管理代码执行过程中使用的内存。其原理是:垃圾收集器会定期(周期性)找出那些不在继续使用的变量,然后释放其内存。但是这个过程不是实时的,因为其开销比较大并且GC时停止响应其他操作,所以垃圾回收器会按照固定的时间间隔周期性的执行。

不再使用的变量也就是生命周期结束的变量,当然只可能是局部变量,全局变量的生命周期直至浏览器卸载页面才会结束。局部变量只在函数的执行过程中存在,而在这个过程中会为局部变量在栈或堆上分配相应的空间,以存储它们的值,然后在函数中使用这些变量,直至函数结束,而闭包中由于内部函数的原因,外部函数并不能算是结束。

还是上代码说明吧:

function fn1() {
    var obj = {name: 'hanzichi', age: 10};
}
function fn2() {
    var obj = {name:'hanzichi', age: 10};
    return obj;
}

var a = fn1();
var b = fn2();

我们来看代码是如何执行的。首先声明了两个函数,分别叫做 fn1fn2,当 fn1 被调用时,进入 fn1 的环境,会开辟一块内存存放对象 {name: 'hanzichi', age: 10},而当调用结束后,出了fn1的环境,那么该块内存会被 JS 引擎中的垃圾回收器自动释放;在 fn2 被调用的过程中,返回的对象被全局变量 b 所指向,所以该块内存并不会被释放。

这里问题就出现了:到底哪个变量是没有用的?所以垃圾收集器必须跟踪到底哪个变量没用,对于不再有用的变量打上标记,以备将来收回其内存。用于标记的无用变量的策略可能因实现而有所区别,通常情况下有两种实现方式:标记清除引用计数。引用计数不太常用,标记清除较为常用。

2. 标记清除

js中最常用的垃圾回收方式就是标记清除。当变量进入环境时,例如,在函数中声明一个变量,就将这个变量标记为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到它们。而当变量离开环境时,则将其标记为“离开环境”。

function test(){
var a = 10 ;       // 被标记 ,进入环境 
var b = 20 ;       // 被标记 ,进入环境
}
test();            // 执行完毕 之后 a、b又被标离开环境,被回收。

垃圾回收器在运行的时候会给存储在内存中的所有变量都加上标记(当然,可以使用任何标记方式)。然后,它会去掉环境中的变量以及被环境中的变量引用的变量的标记(闭包)。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后,垃圾回收器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。
到目前为止,IE9+、Firefox、Opera、Chrome、Safari 的 JS 实现使用的都是标记清除的垃圾回收策略或类似的策略,只不过垃圾收集的时间间隔互不相同。

3. 引用计数

引用计数的含义是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是 1。如果同一个值又被赋给另一个变量,则该值的引用次数加 1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减 1。当这个值的引用次数变成 0 时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。这样,当垃圾回收器下次再运行时,它就会释放那些引用次数为 0 的值所占用的内存。

function test() {
    var a = {};    // a指向对象的引用次数为1
    var b = a;     // a指向对象的引用次数加1,为2
    var c = a;     // a指向对象的引用次数再加1,为3
    var b = {};    // a指向对象的引用次数减1,为2
}

Netscape Navigator3 是最早使用引用计数策略的浏览器,但很快它就遇到一个严重的问题:循环引用。循环引用指的是对象 A 中包含一个指向对象B的指针,而对象 B 中也包含一个指向对象 A 的引用。

function fn() {
    var a = {};
    var b = {};
    a.pro = b;
    b.pro = a;
}
fn();

以上代码 ab 的引用次数都是 2,fn 执行完毕后,两个对象都已经离开环境,在标记清除方式下是没有问题的,但是在引用计数策略下,因为 ab 的引用次数不为 0,所以不会被垃圾回收器回收内存,如果 fn 函数被大量调用,就会造成内存泄露。在 IE7 与 IE8 上,内存直线上升。

我们知道,IE 中有一部分对象并不是原生 JS 对象。例如,其内存泄露 DOM 和 BOM 中的对象就是使用 C++ 以 COM 对象的形式实现的,而 COM 对象的垃圾回收机制采用的就是引用计数策略。因此,即使IE的js引擎采用标记清除策略来实现,但 JS 访问的COM对象依然是基于引用计数策略的。换句话说,只要在 IE 中涉及 COM 对象,就会存在循环引用的问题。

var element = document.getElementById("some_element");
var myObject = new Object();
myObject.e = element;
element.o = myObject;

这个例子在一个 DOM 元素 element 与一个原生js对象 myObject 之间创建了循环引用。其中,变量 myObject 有一个属性 e 指向 element 对象;而变量 element 也有一个属性 o 回指 myObject。由于存在这个循环引用,即使例子中的 DOM 从页面中移除,它也永远不会被回收。

举个栗子:

  • 黄色是指直接被 js变量所引用,在内存里
  • 红色是指间接被 js变量所引用,如上图,refB 被 refA 间接引用,导致即使 refB 变量被清空,也是不会被回收的
  • 子元素 refB 由于 parentNode 的间接引用,只要它不被删除,它所有的父元素(图中红色部分)都不会被删除

另一个例子:

window.onload=function outerFunction(){
    var obj = document.getElementById("element");
    obj.onclick=function innerFunction(){};
};

这段代码看起来没什么问题,但是 obj 引用了 document.getElementById('element'),而 document.getElementById('element')onclick 方法会引用外部环境中的变量,自然也包括 obj,是不是很隐蔽啊。(在比较新的浏览器中在移除Node的时候已经会移除其上的event了,但是在老的浏览器,特别是 IE 上会有这个 bug)

解决办法:

最简单的方式就是自己手工解除循环引用,比如刚才的函数可以这样

myObject.element = null;
element.o = null;

window.onload=function outerFunction(){
    var obj = document.getElementById("element");
    obj.onclick=function innerFunction(){};
    obj=null;
};

将变量设置为 null 意味着切断变量与它此前引用的值之间的连接。当垃圾回收器下次运行时,就会删除这些值并回收它们占用的内存。

要注意的是,IE9+ 并不存在循环引用导致 DOM 内存泄露问题,可能是微软做了优化,或者 DOM 的回收方式已经改变。

4. 内存管理

4.1 什么时候触发垃圾回收?

垃圾回收器周期性运行,如果分配的内存非常多,那么回收工作也会很艰巨,确定垃圾回收时间间隔就变成了一个值得思考的问题。IE6的垃圾回收是根据内存分配量运行的,当环境中存在256个变量、4096个对象、64k的字符串任意一种情况的时候就会触发垃圾回收器工作,看起来很科学,不用按一段时间就调用一次,有时候会没必要,这样按需调用不是很好吗?但是如果环境中就是有这么多变量等一直存在,现在脚本如此复杂,很正常,那么结果就是垃圾回收器一直在工作,这样浏览器就没法儿玩儿了。

微软在 IE7 中做了调整,触发条件不再是固定的,而是动态修改的,初始值和 IE6 相同,如果垃圾回收器回收的内存分配量低于程序占用内存的15%,说明大部分内存不可被回收,设的垃圾回收触发条件过于敏感,这时候把临街条件翻倍,如果回收的内存高于 85%,说明大部分内存早就该清理了,这时候把触发条件置回。这样就使垃圾回收工作职能了很多

4.2 合理的GC方案

1. 基础方案

Javascript 引擎基础GC方案是(simple GC):mark and sweep(标记清除),即:

  1. 遍历所有可访问的对象。
  2. 回收已不可访问的对象。

2. GC的缺陷

和其他语言一样,JS 的 GC 策略也无法避免一个问题:GC 时,停止响应其他操作,这是为了安全考虑。而 Javascript 的 GC 在 100ms 甚至以上,对一般的应用还好,但对于 JS 游戏,动画对连贯性要求比较高的应用,就麻烦了。这就是新引擎需要优化的点:避免GC造成的长时间停止响应。

3. GC优化策略

David 大叔主要介绍了2个优化方案,而这也是最主要的2个优化方案了:

  1. 分代回收(Generation GC)
    这个和Java回收策略思想是一致的,也是V8所主要采用的。目的是通过区分“临时”与“持久”对象;多回收“临时对象”区(young generation),少回收“持久对象”区(tenured generation),减少每次需遍历的对象,从而减少每次GC的耗时。如图:


    这里需要补充的是:对于tenured generation对象,有额外的开销:把它从young generation迁移到tenured generation,另外,如果被引用了,那引用的指向也需要修改。
    这里主要内容可以参考深入浅出Node中关于内存的介绍,很详细~

  2. 增量GC
    这个方案的思想很简单,就是“每次处理一点,下次再处理一点,如此类推”。如图:

    这种方案,虽然耗时短,但中断较多,带来了上下文切换频繁的问题。因为每种方案都其适用场景和缺点,因此在实际应用中,会根据实际情况选择方案。比如:低 (对象/s) 比率时,中断执行GC的频率,simple GC更低些;如果大量对象都是长期“存活”,则分代处理优势也不大。

5. Vue 中的内存泄漏问题

JS 程序的内存溢出后,会使某一段函数体永远失效(取决于当时的 JS 代码运行到哪一个函数),通常表现为程序突然卡死或程序出现异常。

这时我们就要对该 JS 程序进行内存泄漏的排查,找出哪些对象所占用的内存没有释放。这些对象通常都是开发者以为释放掉了,但事实上仍被某个闭包引用着,或者放在某个数组里面。

5.1 泄漏点

  1. DOM/BOM 对象泄漏;
  2. script 中存在对 DOM/BOM 对象的引用导致;
  3. JS 对象泄漏;
  4. 通常由闭包导致,比如事件处理回调,导致 DOM 对象和脚本中对象双向引用,这个是常见的泄漏原因;

5.2 代码关注点

主要关注的就是各种事件绑定场景,比如:

  1. DOM 中的 addEventLisner 函数及派生的事件监听,比如 Jquery 中的 on 函数,Vue 组件实例的 $on 函数;
  2. 其它 BOM 对象的事件监听, 比如 websocket 实例的 on 函数;
  3. 避免不必要的函数引用;
  4. 如果使用 render 函数,避免在 HTML 标签中绑定 DOM/BOM 事件;

5.3 如何处理

  1. 如果在 mounted/created 钩子中使用 JS 绑定了 DOM/BOM 对象中的事件,需要在 beforeDestroy 中做对应解绑处理;
  2. 如果在 mounted/created 钩子中使用了第三方库初始化,需要在 beforeDestroy 中做对应销毁处理(一般用不到,因为很多时候都是直接全局 Vue.use);
  3. 如果组件中使用了 setInterval,需要在 beforeDestroy 中做对应销毁处理;

5.4 在 vue 组件中处理 addEventListener

调用 addEventListener 添加事件监听后在 beforeDestroy 中调用 removeEventListener 移除对应的事件监听。为了准确移除监听,尽量不要使用匿名函数或者已有的函数的绑定来直接作为事件监听函数。

mounted() {
    const box = document.getElementById('time-line')
    this.width = box.offsetWidth
    this.resizefun = () => {
      this.width = box.offsetWidth
    }
    window.addEventListener('resize', this.resizefun)
  },
  beforeDestroy() {
    window.removeEventListener('resize', this.resizefun)
    this.resizefun = null
  }

5.5 观察者模式引起的内存泄漏

在 spa 应用中使用观察者模式的时候如果给观察者注册了被观察的方法,而没有在离开组件的时候及时移除,可能造成重复注册而内存泄漏;

举个栗子:
进入组件的时候 ob.addListener("enter", _func),如果离开组件 beforeDestroy 的时候没有 ob.removeListener("enter", _func),就会导致内存泄漏。

更详细的栗子参考:德州扑克栗子

5.6 上下文绑定引起的内存泄漏

有时候使用 bind/apply/call 上下文绑定方法的时候,会有内存泄漏的隐患。

var ClassA = function(name) {
  this.name = name
  this.func = null
}

var a = new ClassA("a")
var b = new ClassA("b")

b.func = bind(function() {
  console.log("I am " + this.name)
}, a)

b.func()    // 输出: I am a

a = null           // 释放a
//b = null;        // 释放b
//b.func = null;   // 释放b.func

function bind(func, self) {    // 模拟上下文绑定
  return function() {
    return func.apply(self)
  }
}

使用 chrome dev tool > memory > profiles 查看内存中 ClassA 的实例数,发现有两个实例,ab。虽然 a 设置成 null 了,但是 b 的方法中 bind 的闭包上下文 self 绑定了 a,因此虽然 a 释放,但是 b/b.func 没有释放,闭包的 self 一直存在并保持对 a 的引用。


网上的帖子大多深浅不一,甚至有些前后矛盾,在下的文章都是学习过程中的总结,如果发现错误,欢迎留言指出~

参考:

  1. 跟我学习javascript的垃圾回收机制与内存管理
  2. App之性能优化
  3. Vue Web App 内存泄漏-调试和分析
  4. 搞定JavaScript内存泄漏

推介阅读:

  1. 雅虎网站页面性能优化的34条黄金守则
  2. 用 Chrome 开发者工具分析 javascript 的内存回收(GC)
  3. JS内存泄漏排查方法——Chrome Profiles
  4. Javascript典型内存泄漏及chrome的排查方法

PS:欢迎大家关注我的公众号【前端下午茶】,一起加油吧~

另外可以加入「前端下午茶交流群」微信群,长按识别下面二维码即可加我好友,备注加群,我拉你入群~

查看原文

liuy666 收藏了文章 · 9月24日

前端爬坑日记之vue内嵌iframe并跨域通信

由于该项目是基于原本的安卓app,做的微信h5,所以原来的使用webview的页面现在需要在vue中实现,那就是使用iframe
查看了很多很多文档,其中这一篇是很有价值的 https://gist.github.com/pboji...

下面将3天的爬坑最终以问答的方式总结如下:

1、Vue组件中如何引入iframe?

2、vue如何获取iframe对象以及iframe内的window对象?

3、vue如何向iframe内传送信息?

4、iframe内如何向外部vue发送信息?

1、Vue组件中如何引入iframe?

<template>
  <div class="act-form">
    <iframe :data-original="src"></iframe>
  </div>
</template>

<script>

export default {
  data () {
    return {
      src: '你的src'
    }
  }
}
</script>
如上,直接通过添加iframe标签,src属性绑定data中的src,第一步引入就完成了

2、vue如何获取iframe对象以及iframe内的window对象?

在vue中,dom操作比不上jquery的$('#id')来的方便,但是也有办法,就是通过ref
<template>
  <div class="act-form">
    <iframe :data-original="src" ref="iframe"></iframe>
  </div>
</template>

<script>

export default {
  data () {
    return {
      src: '你的src'
    }
  },
  mounted () {
    // 这里就拿到了iframe的对象
    console.log(this.$refs.iframe)
  }
}
</script>

然后就是获取iframe的window对象,因为只有拿到这个对象才能向iframe中传东西

<template>
  <div class="act-form">
    <iframe :data-original="src" ref="iframe"></iframe>
  </div>
</template>

<script>

export default {
  data () {
    return {
      src: '你的src'
    }
  },
  mounted () {
    // 这里就拿到了iframe的对象
    console.log(this.$refs.iframe)
    // 这里就拿到了iframe的window对象
    console.log(this.$refs.iframe.contentWindow)
  }
}
</script>

3、vue如何向iframe内传送信息?

通过postMessage,具体关于postMessage是什么,自己去google,

我的理解postMessage是有点类似于UDP协议,就像短信,是异步的,你发信息过去,但是没有返回值的,只能内部处理完成以后再通过postMessage向外部发送一个消息,外部监听message

为了让postMessage像TCP,为了体验像同步的和实现多通信互不干扰,特别制定的message结构如下
{
  cmd: '命令',
  params: {
    '键1': '值1',
    '键2': '值2'
  }
}

通过cmd来区别这条message的目的

具体代码如下

<template>
  <div class="act-form">
    <iframe :data-original="src" ref="iframe"></iframe>
    <div @click="sendMessage">向iframe发送信息</div>
  </div>
</template>

<script>

export default {
  data () {
    return {
      src: '你的src',
      iframeWin: {}
    }
  },
  methods: {
    sendMessage () {
      // 外部vue向iframe内部传数据
      this.iframeWin.postMessage({
        cmd: 'getFormJson',
        params: {}
      }, '*')
    },
  },
  mounted () {
    // 在外部vue的window上添加postMessage的监听,并且绑定处理函数handleMessage
    window.addEventListener('message', this.handleMessage)
    this.iframeWin = this.$refs.iframe.contentWindow
  },
  handleMessage (event) {
    // 根据上面制定的结构来解析iframe内部发回来的数据
    const data = event.data
    switch (data.cmd) {
      case 'returnFormJson':
        // 业务逻辑
        break
      case 'returnHeight':
        // 业务逻辑
        break
    }
  }
}
</script>

4、iframe内如何向外部vue发送信息?

现在通过点击“向iframe发送信息”这个按钮,从外部vue中已经向iframe中发送了一条信息

{
  cmd: 'getFormJson',
  params: {}
}

那么iframe内部如何处理这个信息呢?

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>iframe Window</title>
    <style>
        body {
            background-color: #D53C2F;
            color: white;
        }
    </style>
</head>
<body>

    <h1>Hello there, i'm an iframe</h1>

    <script>
        // 向父vue页面发送信息
        window.parent.postMessage({
            cmd: 'returnHeight',
            params: {
              success: true,
              data: document.body.scrollHeight + 'px'
            }
        }, '*');

        // 接受父页面发来的信息
        window.addEventListener("message", function(event){
          var data = event.data;
          switch (data.cmd) {
            case 'getFormJson':
                // 处理业务逻辑
                break;
            }
        });
    </script>
</body>
</html>

至此内部的收发信息已经解决了,外部的收发也已经解决了,快去解决你的问题吧

在这里先直接给出我项目的源码

<template>
  <div class="act-form">
    <div class="nav">
      <img data-original="https://cxkccdn.oss-cn-shanghai.aliyuncs.com/lesai_img/icon_back_white.png" @click="back()">
      <div class="title">报名</div>
    </div>
    <div class="iframe-out">
      <iframe :data-original="src" ref="iframe" @load="iframeLoad"></iframe>
    </div>
    <div v-if="isLoaded" class="send-form"><div class="send" @click="sendMessage()">提交</div></div>
  </div>
</template>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style lang="sass" rel="stylesheet/sass">
  @import "style.scss";
</style>

<script>
import { Toast, Indicator } from 'mint-ui'
import api from '@/utils/api'

export default {
  data () {
    return {
      src: '',
      iframeWin: null,
      isLoaded: false
    }
  },
  created () {
    let matchFamily = this.$store.state.matchFamily
    this.src = process.env.BASE_URL + '/matches/' + matchFamily.match.id + '/act/' + matchFamily.act.id + '/joinweb?token=' + this.$store.state.token
  },
  mounted () {
    window.addEventListener('message', this.handleMessage)
    this.iframeWin = this.$refs.iframe.contentWindow
    // 开启加载动画
    Indicator.open({
      text: '努力加载中...',
      spinnerType: 'triple-bounce'
    })
  },
  methods: {
    back () {
      this.$router.push('/actIntro')
    },
    sendMessage () {
      this.iframeWin.postMessage({
        cmd: 'getFormJson',
        params: {}
      }, '*')
    },
    iframeLoad () {
      // 关闭加载动画
      Indicator.close()
    },
    async handleMessage (event) {
      const data = event.data
      switch (data.cmd) {
        case 'returnFormJson':
          if (data.params.success) {
            // 调用报名方法
            await this.enroll(data.params.data)
          } else {
            console.log('returnFormJson失败')
            console.log(data.params)
          }
          break
        case 'returnHeight':
          if (data.params.success) {
            this.$refs.iframe.height = data.params.data
            this.isLoaded = true
          }
          break
      }
    },
    async enroll (data) {
      let matchFamily = this.$store.state.matchFamily
      let result = await api.enroll(matchFamily.match.id, matchFamily.act.id, data)
      if (result.success) {
        if (result.data.status === 'no_pay') {
          // 更新缓存
          let resultMatch = await api.match(matchFamily.match.id, {})
          if (resultMatch.success) {
            this.$store.commit('SET_CURRENT_MATCH', resultMatch.data)
          }
          Toast({
            message: '报名成功',
            position: 'bottom'
          })
          this.$router.push('/match/' + matchFamily.match.id + '/mdetail')
        } else {
          console.log('需要跳转到支付页面')
        }
      }
    }
  }
}
</script>

欢迎大家来看看我的博客 https://www.windzh.com

查看原文

liuy666 收藏了文章 · 9月24日

npm模块管理进阶 — npm-check + cnpm 构建包更新环境

前言

近期在项目中准备更新一下npm依赖包,可一尝试,惊了!批量更新还真麻烦。各种包要挨个更新,就算直接更改package.json也挺费事。
于是度娘到了npm-check,然后琢磨了一下,结合cnpm构建了一个本人很满意的包更新环境。遂决定写一篇博文,分享给大家。
阅读本文需要一定node,npm,webpack基础(不高,挺低 <( ̄ˇ ̄)/)
本文为我个人理解,一家之言,如有不当或错误的地方欢迎大家指正,谢谢!
博文地址:npm模块管理进阶 — npm-check + cnpm 构建包更新环境

1. npm-check

npm 是node下的包管理工具,给我们提供了强大的包管理功能,简化了项目的代码部署过程。但是,npm也不是尽善尽美,批量更新时便很捉急。
npm-check便应运而生。
npm-check是一个npm包更新工具。它还可以检查项目的npm依赖包是否有更新,缺失,错误以及未使用等情况。其 几大主要优势如下:

1.提供图形化界面,还有emoji,点个赞(不用对着黑白界面简直良心啊!我也想用emoji写啊!:-))
2.批量更新依赖包,还兼职检测包使用情况
3.项目下更新支持自动检测包的 "dependencies" 和"devDependencies"并更新"package.json"信息   !

npm-check

1.1 npm-check安装

> npm install -g npm-check //全局安装。项目下安装可自行选择
> npm install npm-check    //项目下安装,项目根目录执行

1.2 npm-check项目依赖包更新

(1)查看包更新信息,会有小黄脸提示你包的相关情况(需更新,缺失,错误以及未使用等)(表情包大牛。。。)

> npm-check

npm-check-2

(2)更新包。分类别展示,使用空格选择包,然后enter开始更新。自动更新package.json内的相关包信息

> npm-check -u //-update 

npm-check-3

基础就是这样,下面是npm-check指令列表:

-u, --update       显示一个交互式UI,用于选择要更新的模块,并自动更新"package.json"内包版本号信息
-g, --global       检查全局下的包
-s, --skip-unused  忽略对未使用包的更新检查
-p, --production   忽略对"devDependencies"下的包的检查
-d, --dev-only     忽略对"dependencies"下的包的检查
-i, --ignore       忽略对指定包的检查.
-E, --save-exact   将确切的包版本存至"package.json"(注意,此命令将存储'x.y.z'而不是'^x.y.z')

1.3 npm-check的问题

npm-check更新包时是根据版本号动态生成更新语句并执行。其本质是仍然npm install指令:

npm-check-4

所以,问题不是npm install,而是国内的问题。介于网络等因素,国内用户更倾向于使用cnpm,cnpm也比较稳定和快速。
下面就介绍如何使用让npm-check使用cnpm执行更新。

3.npm-check + cnpm

cnpm 是阿里提供的一个完整 npmjs.org 镜像,便于国内用户使用npm。这里就不过多赘述了。
准备:安装好cnpm,npm-check
本文将介绍两种方法将cnpm加入npm-check,以及其原理,具体如下。


3.1 两种方法简述

1. 源码修改法

(1) 找到npm-check包下的"npm-check\lib\""npm-check\lib-es5\"下的cli.js文件。
(2) 对文件内代码"options"的属性"installer"进行修改:

installer: process.env.NPM_CHECK_INSTALLER || 'npm'  //改前
installer: process.env.NPM_CHECK_INSTALLER || 'cnpm' //改后

2.修改环境变量值
NPM_CHECK_INSTALLER设为cnpm。后文将介绍三种修改方法,效果有些差别。

3.2 两种方法原理

1. 源码修改法

npm-check目录结构如下。一眼就看见"./lib""./lib-es5"目录结构完全相同,是有什么原因吗?没错,后面看了各位就明白了。
npm-check-5

首先最基础的,我们先来看npm-check"package.json"。(里面"dependencies"等有些属性与本文无关,我删除了其内容,以免代码过长)

{
  "name": "npm-check",
  "version": "5.4.5",
  "description": "Check for outdated, incorrect, and unused dependencies.",
  "main": "lib",
  "bin": {
    "npm-check": "bin/cli.js"
  },
  "engines": {
    "node": ">=0.11.0"
  },
  "types": "./index.d.ts",
  "typings": "./index.d.ts",
  "scripts": {
    "lint": "xo ./lib/*.js",
    "test": "npm run lint && ./bin/cli.js || echo Exit Status: $?.",
    "transpile": "babel lib --out-dir lib-es5",
    "watch": "babel lib --out-dir lib-es5 --watch",
    "prepublish": "npm run transpile"
  },
  "xo": {
    "space": 4,
    "rules": {
      "no-warning-comments": [
        0
      ],
      "global-require": [
        0
      ]
    }
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/dylang/npm-check.git"
  },
  "keywords": [],
  "author": {
    "name": "Dylan Greene",
    "email": "dylang@gmail.com"
  },
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/dylang/npm-check/issues"
  },
  "homepage": "https://github.com/dylang/npm-check",
  "files": [
    "bin",
    "lib",
    "lib-es5"
  ],
  "dependencies": {},
  "devDependencies": {},
  "_from": "npm-check@5.4.5",
  "_resolved": "http://registry.npm.taobao.org/npm-check/download/npm-check-5.4.5.tgz"
}

可以看到"bin"属性值为{"npm-check": "bin/cli.js"},其作用是将"bin/cli.js"链接到"npm-check"
当在命令行执行"npm-check",便会执行"bin/cli.js"。接着我们看看"bin/cli.js"到底有什么玄机。
在查看之前,你可能会觉得里面是一大串代码,看着就头晕,然而事实是,里面只有9行:

#!/usr/bin/env node

var isEs2015;
try {
    isEs2015 = new Function('() => {}');
} catch (e) {
    isEs2015 = false;
}
isEs2015 ? require('../lib/cli') : require('../lib-es5/cli');

代码的意思很明显,判断js环境是否支持es6(Es2015)。

isEs2015 = new Function('() => {}');

通过es6的箭头函数判断js环境,支持则isEs2015true,导入'../lib/cli',否则导入'../lib-es5/cli'
所以上文提到的"./lib""./lib-es5"目录结构完全相同也就明白了,因为他们分别是es6es5npm-check的实现。
接下来我们就分析"./lib/cli,js"的源码,毕竟es6是趋势嘛,"./lib-es5/cli.js"异曲同工。
"./lib/cli,js":

#!/usr/bin/env node
'use strict';

const meow = require('meow');
const updateNotifier = require('update-notifier');
const isCI = require('is-ci');
const createCallsiteRecord = require('callsite-record');
const pkg = require('../package.json');
const npmCheck = require('./index');
const staticOutput = require('./out/static-output');
const interactiveUpdate = require('./out/interactive-update');
const debug = require('./state/debug');
const pkgDir = require('pkg-dir');

updateNotifier({pkg}).notify();

const cli = meow({...});           //"npm-check -u,-g等指令的相关设计"

const options = {
    cwd: cli.input[0] || cli.flags.dir,
    update: cli.flags.update,
    global: cli.flags.global,
    skipUnused: cli.flags.skipUnused,
    ignoreDev: cli.flags.production,
    devOnly: cli.flags.devOnly,
    saveExact: cli.flags.saveExact,
    specials: cli.flags.specials,
    emoji: cli.flags.emoji,
    installer: process.env.NPM_CHECK_INSTALLER || 'npm',
    debug: cli.flags.debug,
    spinner: cli.flags.spinner,
    ignore: cli.flags.ignore
};

if (options.debug) {...}         //"是否显示调试输出"

npmCheck(options)                //"根据options的数据运行npm-check"
    .then(currentState => {...})
    .catch(err => {...});

具体代码过长,我以...代替了,我将对里面里面函数的理解注释在了后面,以作参考。
下面就是最重要的"options"对象:

const options = {
    cwd: cli.input[0] || cli.flags.dir,
    update: cli.flags.update,
    global: cli.flags.global,
    skipUnused: cli.flags.skipUnused,
    ignoreDev: cli.flags.production,
    devOnly: cli.flags.devOnly,
    saveExact: cli.flags.saveExact,
    specials: cli.flags.specials,
    emoji: cli.flags.emoji,
    installer: process.env.NPM_CHECK_INSTALLER || 'npm',
    debug: cli.flags.debug,
    spinner: cli.flags.spinner,
    ignore: cli.flags.ignore
};

里面定义的是 npm-check 运行时的一些默认配置。再看 "installer" 属性:

installer: process.env.NPM_CHECK_INSTALLER || 'npm'

这个值定义了 npm-check 在更新包时所使用的包管理工具:process.env.NPM_CHECK_INSTALLER 或者npm
process.env.NPM_CHECK_INSTALLER 是环境变量,与第二种方法有关,这里先不做介绍。
因为所以process.env.NPM_CHECK_INSTALLER一般未定义,所以将取第二个值npm。将其改为cnpm即可使用cnpm install("./lib-es5"同上)

npm-check-6

再说一些:我在实际源码分析时还挺麻烦的,上面是为大家做介绍,比较简单。在这里简述一下我个人阅读源码之后的理解,供大家参考:

  1. 首先"./lib/out/install-packages.js"中定义了install()函数,负责npm-check进行包更新信息分析并输出更新语句的功能,然后导出install()函数。
  2. "./lib/out/interactive-updates.js"引入install()并加上对图形化界面的实现后导出。
  3. 最后"./lib/cli.js"引入并结合options实现完整包更新的功能。

2. 修改环境变量

修改源码的方法毕竟不太好,修改环境变量则相对更优。
回到前面,"./lib/cli,js"里:

installer: process.env.NPM_CHECK_INSTALLER || 'npm'

要操作的就是 process.env.NPM_CHECK_INSTALLER

> 'process' : Node 的一个全局对象,提供当前 Node 进程的信息。
> 'process.env' : 存储着"当前Shell"的环境变量。
通常的做法是,新建一个环境变量"NODE_ENV",用它确定当前所处的开发阶段;
生产阶段设为production,开发阶段设为develop或staging;
然后在脚本中读取process.env.NODE_ENV判断开发情况。

process.env.NPM_CHECK_INSTALLER就是NPM_CHECK_INSTALLER变量。
因为这是一个自定义变量,所以缺省环境是不存在这个值的,修改源码法才能成功。当定义了这个变量,installer的第二个参数便失效了。(当然,||规则,若NPM_CHECK_INSTALLEfalse结果的值,第二个参数仍会生效)

2.1 node变量设置法

> set NPM_CHECK_INSTALLER=cnpm //win端,cnpm不要加引号,不然是string值,是错误的

npm-check-7

进入node查看,设置成功。之后执行npm-check更新时使用的便是cnpm

npm-check-8

注意:此设置方法下环境变量的生命周期为当前shell。关闭终端或者用另一个shell等都读取不到此设置,环境不同!

2.2 项目"package.json"配置

此方法针对具体项目进行配置,在一个项目里进行一次配置即可。另外大家也可以自行进行优化。
"package.json"中的"scripts"对象可以自定义脚本命令。其键是运行的事件名,值是要运行的命令,通过 npm run ***运行。将"scripts"增加新属性"nc-u"如下:

"scripts": {
    "start": "webpack-dev-server",
    "nc-u":"set NPM_CHECK_INSTALLER=cnpm&& npm-check -u"
  }

使用npm run nc-u即可一步执行。有几点需要注意:
1."nc-u"自然是自己命名,合乎规范的都可以;
2."set NPM_CHECK_INSTALLER=cnpm&& npm-check -u"也很明了。先执行set NPM_CHECK_INSTALLER=cnpm,再执行npm-check -u
3.注意NPM_CHECK_INSTALLER=cnpm&&&&需紧跟cnpm,如&&前有空格,则空格也会赋值给NPM_CHECK_INSTALLER,执行到更新包时会出错。
4.脚本命令中环境变量值的生命周期在命令执行期间。此命令执行完,NPM_CHECK_INSTALLER便会被回收,还原为undefined,不污染全局。

2.3 项目"package.json"配置进阶 — 使用cross-env

上面2.2配置中使用&&,set等是因为window环境需要,如果转为Mac那便会出错了,不同平台还有诸多细节不同。怎样才能跨平台使用呢?这时候我们就可以使用cross-env
cross-env是一个解决跨平台设置"scripts"的工具,使用之后便不用考虑平台问题了。基本大部分项目都默认添加了cross-env

> npm install --save-dev cross-env //本地安装,写入依赖
//package.json

"scripts": {
    "start": "webpack-dev-server",
    "nc-u":"cross-env NPM_CHECK_INSTALLER=cnpm npm-check -u"
  },
  "devDependencies": {
    "cross-env": "^5.0.5"
  }

在句首加上cross-env即可,执行时cross-env会对指令进行处理。
注意:
使用cross-env时使用&&会改变前后语句环境,即每一语句段都有自己的环境,即环境变量设置会失效。慎用&&

4.总结

本文首先简单介绍了npm-check及其用法,然后介绍了如何结合cnpm进行cnpm install以及方法的原理。
至此,结合了npm-check模块更新工具和cnpm国内镜像,在模块更新的操作和速度上都已获得提升,模块更新环境搭建完毕!

5.后记

断断续续写了也快一天了,写完之后校对,一下子都快1点钟了,感觉还在早上刚开始写的时候一样。总算是写完了这篇博文。
其实这是我第一篇正式意义上的博文,莫名地感觉有种莫名的感觉,写完之后还有点不舍..W( ̄_ ̄)W,但总归是完成了自己的写作,还是很开心的!
初次写作,还有许多遗憾的地方未能做好,甚是可惜。特别是看了npm-check后,自己特别想用几个小黄脸表情:-)! 但是不支持啊啊啊啊!憾
最后,初次写作,有不当或错误的地方希望各位见谅,更欢迎大家指出,让我能改正。
另外大家觉得不错的话,希望能点个赞,谢谢!

查看原文

liuy666 收藏了文章 · 9月24日

webpack 构建多页面应用——初探

如何使用webpack构建多页面应用,这是一个我一直在想和解决的问题。网上也给出了很多的例子,很多想法。猛一看,觉得有那么点儿意思,但仔细看也就那样。

使用webpack这个构建工具,可以使我们少考虑很多的问题。

我们常见的单页面应用只有一个页面,它考虑问题,解决问题围绕着中心化去解决,因此很多麻烦都迎刃而解。如果你使用过vue.js,那么想必你一定用过vue-router,vuex,它们就是典型的中心化管理模式,当然还有很多,这里不一一列举了。

而多页面应用,我们不能再按照中心化模式的路走了,因为行不通,这也是很多人认为多页面应用不好做,或者干脆认为webapck只能做单页面应用,而不能做多页面应用的原因。

所以,我要说明的第一点儿是:不要用做单页面应用的思维来做多页面应用。

单页面中的模块共享和多页面的模块共享的区别

  1. 单页面的模块共享,其实是代码块在同一个页面的不同位置的重复出现;而多页面应用的代码块儿共享需要实现的不仅是同一个页面的共享,还要做到跨页面的共享。

    所以,第一个要解决的问题是:不同页面的代码块共享如何实现?

  2. 单页面的路由管理,其实是根据用户的触发条件来实现不同的代码块的显隐;而多页面应用的路由管理则不然,它实现的是页面的跳转。

    所以,第二个要解决的问题是:所页面应用的导航该如何做?

  3. 单页面的状态管理,很受开发者喜好。单页面是一个页面,所以页面中的数据状态的管理操作起来还算得心应手,那么多页面应用的呢,显然依靠它自身很难实现。

所以,第三个要解决的问题是:多页面应用的状态管理如何做?

注:这个问题问的其实有点儿傻,如果你做的是dom操作的多页面儿应用,就不用做状态管理了。如果你还是使用想vue.js这样的库,你就需要考虑要不要再用做多页面的状态管理了,因为此法儿就是为单页面应用做的,多页面儿行不通。

多页面应用的探索

入口(entry):

webpack对入口不仅可以定义单个文件,也可以定义多个文件。

熟悉当页面应用开发的对于下面的代码应该不会陌生吧?

module.exports = {
  entry: './src/index.js',
  ···
}

我第一次接触真正的单页面应用项目使用的就是angualrjs,使用的构建工具使webapck+gulp,其中的webpack.config.js 中的看到的入口文件代码就是它。

后来,接触到的是数组形式,代码如下:

module.exports = {
  entry: ['./src/index.js', 'bootstrap']
  ···
}

这样,将bootstrap和入口文件一起引用,就可以在任何一个代码块中使用boostrap。

再后来,接触到的是对象形式,代码如下:

module.exports = {
  main: './src/index.js'
  ···
}

这样做的目的是为了给输出的文件指定特定的名字。

再后来,就是做多页面应用,就需要用到如下的代码:

module.exports = {
  entry: {
    index: './src/index.js',
    aboutUs: './src/aboutus.js',
    contactUs: './src/contactus.js'
  }
}

为了引入第三方库,我们可以像如下这样做:

module.exports = {
  entry: {
    index: ['./src/index.js', 'loadsh'],
    aboutUs: './src/aboutus.js',
    contactUs: ['./src/contactus.js', 'lodash']
  }
}

webpack3.x的探索

但为了共享模块代码,我们需要像下面这这样做:

const CommonsChunkPlugin = require('webpack').optimization.CommonsChunkPlugin
module.exports = {
  entry: {
    index: ['./src/index.js', './src/utils/load.js', 'loadsh'],
    aboutUs: ['./src/aboutus.js', 'loadsh'],
    contactUs: ['./src/contactus.js','./src/utils/load.js', 'lodash']
  },
  plugins: [
        new CommonsChunkPlugin({
            name: "commons",
            filename: "commons.js",
            chunks: ["index", "aboutUs", "contactUs"]
        })
  ]
}

这样型就会形成如下所示的项目目录结构:

├── src
│ ├── common // 公用的模块
│ │ ├── a.js
│ │ ├── b.js
│ │ ├── c.js
│ │ ├── d.js
│ ├── uttils // 工具
│ │ ├── load.js // 工具代码load.js
│ ├── index.js // 主模块index.js (包含a.js, b.js, c.js, d.js)
│ ├── aboutUs.js // 主模块aboutus.js (包含a.js, b.js)
│ ├── contactUs.js // 主模块contactus.js (包含a.js, c.js)
├── webpack.config.js // css js 和图片资源
├── package.json
├── yarn.lock

但是这个内置插件的局限性比较大。正如上面所使用的那样,它只会提取chunks选项所匹配的模块共有的代码块。就如同上面代码表示的那样,它只会提取pindex, aboutUs, contactUs共有的代码块loadsh,而不会提取index, contactUs共有的代码块load.js

当然,一般的第三方库,我们也不这样使用,而是像下面这样使用:

const CommonsChunkPlugin = require('webpack').optimization.CommonsChunkPlugin
module.exports = {
  entry: {
    index: ['./src/index.js', './src/utils/load.js'],
    aboutUs: ['./src/aboutus.js'],
    contactUs: ['./src/contactus.js','./src/utils/load.js'],
    vendors: ['lodash']
  },
  externals: {
    commonjs: "lodash",
    root: "_"
  },
  plugins: [
        new CommonsChunkPlugin({
            name: "commons",
            filename: "commons.js",
            chunks: ["index", "aboutUs", "contactUs"]
        })
  ]
}

对于web应用最终的目的是:匹配生成不同的html页面。

这里我们要使用的就是html-webpack-plugin

首先,需要安装html-webpack-plugin

yarn add --dev html-webpack-plugin

然后引入插件,并配置如下:

...
const HtmlWebapckPlugin = require('html-webpack-plugin');
...
  plugins: [
        ...
    new HtmlWebapckPlugin({
      filename: 'index.html',
      chunks: ['vendors', 'commons', 'index']
    }),
    new HtmlWebapckPlugin({
      filename: 'aboutUs.html',
      chunks: ['vendors', 'commons', 'aboutUs']
    }),
    new HtmlWebapckPlugin({
      filename: 'contactUs.html',
      chunks: ['commons', 'contactUs']
    })
  ],
  ...

这样一个基于webpack3.x的多页面框架就有了基本的样子。

webpack4.x的探索

而使用webpack4.x则完全不同,它移除了内置的CommonsChunkPlugin插件,引入了SplitChunksPlugin插件,这个插件满足了我们的需要,弥补了CommonsChunkPlugin的不足。

如果你想要解决之前的不足,去提取index, contacUs共有的模块,操作起来会很简单。正如上面的所列举的那样,我们有三个入口点index, aboutUs, contactUsSplitChunksPlugin 插件会首先获取这三个入口点共有的代码块,然后建立一个文件,紧接着获取每两个入口点的共有代码块,然后将每个入口点独有的代码块单独形成一个文件。如果你使用了第三方库,就像上面我们使用的loadsh,它会将第三方入口代码块单独打包为一个文件。

配置文件webpack.config.js需要增加如下的代码:

···
optimization: {
  splitChunks: {
    chunks: 'all',
    maxInitialRequests: 20,
    maxAsyncRequests: 20,
    minSize: 40
  }
}
···

因为SplitChunksPlugin可以提取任意的入口点之间的共同代码,所以,我们就不需要使用vendors入口节点了。那么,为匹配生成不同的页面代码可以修改成如下:

const HtmlWebapckPlugin = require('html-webpack-plugin')
···
    plugins: [
      new HtmlWebapckPlugin({
        filename: 'index.html',
        chunks: ['index']
      }),
      new HtmlWebapckPlugin({
        filename: 'aboutUs.html',
        chunks: ['aboutUs']
      }),
      new HtmlWebapckPlugin({
        filename: 'contactUs.html',
        chunks: ['contactUs']
      }),
    ]
···

可以发现结果越来越接近我们所想。但是这里还是存在一个问题,第三方库loadsh因为在入口点index, aboutUs中被分别引入,但是构建的结果却输出了两个第三方库文件,这不是我们想要的。这个问题怎么解决呢,因为html-webpack-plugin插件的chunks选项,支持多入口节点,所以,我们可以再单独创建一个第三方库的入口节点vendors。配置代码修改如下:

...
    entry: {
      index: ['./src/index.js', './src/utils/load.js'],
      aboutUs: ['./src/aboutUs.js'],
      contactUs: ['./src/contactUs.js','./src/utils/load.js'],
      vendors: ['loadsh']
    },
    ...
    plugins: [
      new HtmlWebapckPlugin({
        filename: 'index.html',
        chunks: ['index', 'vendors']
      }),
      new HtmlWebapckPlugin({
        filename: 'aboutUs.html',
        chunks: ['aboutUs', 'vendors']
      }),
      new HtmlWebapckPlugin({
        filename: 'contactUs.html',
        chunks: ['contactUs']
      }),
    ],
...

注意:如果不同的入口点儿之间有依赖关系,如上面的indexvendors之间,因为index依赖于vendors,所以vendors要置于index之前。

这篇文章,说到这里基本上已经结束了。当然,webpack多页面应用的知识点还没有讲完,这些内容会放在后续的文章中详解。

源代码

webpack3.x multi-page

webpack4.x multi-page

构建多页面应用系列文章

查看原文

liuy666 收藏了文章 · 9月24日

webpack@v4升级踩坑

之前看到各大公众号都在狂推 webpack 新版发布的相关内容,之前就尝试了升级,由于部分插件的原因,未能成功,现在想必过了这么久已经可以了,今天就来试一下在我的项目中升级会遇到哪些坑。

查阅更新日志

在安装更新之前,先大致浏览了一下更新日志,对大部分用户来说迁移上需要注意的应该就是这些点:

  • 在命令行界面运行打包指令需要安装 webpack-cli
  • 打包需要指定打包模式 production or development ,在不同模式下会添加不同的默认配置, webpack.DefinePlugin 插件的 process.env.NODE_ENV 的值不需要再定义,将根据模式自动添加;
  • 不再需要在 plugin 中设置 new webpack.optimize.UglifyJsPlugin ,只需要在配置中设置开关即可,并且 production 模式自动开启,可以通过 optimization.minimizer 指定其他压缩库;
  • 删除了 CommonsChunkPlugin ,功能已迁移至 optimization.splitChunks ,optimization.runtimeChunk

迁移

  1. 安装最新的 webpackwebpack-cliwebpack-dev-server
  2. 为开发中和发布分别配置 mode ,删除 webpack.DefinePlugin 配置,并且去掉 package.json 中启动脚本的 NODE_ENV 区别环境变量定义;
  3. 去掉 new webpack.optimize.UglifyJsPluginModuleConcatenationPlugin 配置。

爬坑

别慌

  1. 在这些配置好之后我遇到的第一个问题就是打包时 extract-text-webpack-plugin 插件炸了!这里提供了这里有两种解决方案:

    • 方法一:安装指定 extract-text-webpack-plugin 版本 @next
    • 方法二:使用 mini-css-extract-plugin 替代。

      如果使用方法二注意在发布打包时需要指定 css 压缩库配置,并且需要同时写入 js 压缩库,因为你一旦指定了 optimization.minimizer 就会弃用内置的代码压缩:

      /* webpack.config.js */
      const MiniCssExtractPlugin = require('mini-css-extract-plugin');
      
      module.exports = () => {
        const config = {
          module: {
            rules: [
              {
                test: /\.css$/,
                use: [
                  MiniCssExtractPlugin.loader,
                  'css-loader?importLoaders=1',
                  'postcss-loader'
                ]
              },
              {
                test: /\.less$/,
                use: [
                  MiniCssExtractPlugin.loader,
                  'css-loader?importLoaders=1',
                  'postcss-loader',
                  'less-loader'
                ]
              }
            ]
          },
          resolve: {
            extensions: ['.js', '.jsx', '.less']
          }
        };
        
        if (process.env.NODE_ENV === 'development') {
          config.module.rules[0].use = [
            'css-hot-loader',
            MiniCssExtractPlugin.loader,
            'css-loader?importLoaders=1',
            'postcss-loader'
          ];
          config.module.rules[1].use = [
            'css-hot-loader',
            MiniCssExtractPlugin.loader,
            'css-loader?importLoaders=1',
            'postcss-loader',
            {
              loader: 'less-loader',
              options: {
                modifyVars: theme
              }
            }
          ];
        }
      
        return config;
      };
      
      /* webpack.config.prod.js */
      const merge = require('webpack-merge');
      const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
      const MiniCssExtractPlugin = require('mini-css-extract-plugin');
      const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
      const webpackBaseConfig = require('./webpack.config')();
      
      module.exports = merge(webpackBaseConfig, {
        mode: 'production',
        optimization: {
          minimizer: [
            new UglifyJsPlugin({
              cache: true,
              parallel: true,
              uglifyOptions: {
                compress: {
                  warnings: false,
                  drop_debugger: true,
                  drop_console: false
                }
              }
            }),
            new OptimizeCSSAssetsPlugin({})
          ]
        },
        plugins: [
          new MiniCssExtractPlugin({
            filename: 'css/[name].css'
          })
        ]
      });
  2. happypack 炸了,小场面,升级就好 @5.0.0-beta.3happypackextract-text-webpack-plugin 搭配使用更佳,mini-css-extract-plugin 未测试)。
  3. webpack-browser-plugin 炸了,小场面,弃用就好,然后在 devServer 中配置 openopenPage
  4. 上面的配置中可以看到我使用判断语句 process.env.NODE_ENV === 'development' 在开发配置中加入了 css-hot-loader ,但是这里实际上是获取到的是 undefined ,咦?这是什么鬼?查阅更新日志是怎么说的:

    process.env.NODE_ENV are set to production or development (only in built code, not in config)

    意思就是说我们在使用的工程项目代码中会获取到这个变量,但是打包配置中使用这个变量还是获取不到的,我也实际验证了这个结果,so,我在 package.json 的开发启动脚本中还是加上了 NODE_ENV='development'

最后

总体来说现在的升级时机已经成熟,大多需要用到的功能和插件都有平滑的升级或替代方案,建议在开始升级前安装最新发布的插件版本,也可以参考下我的项目配置react-with-mobx-template

还有对插件的一些 API 也做了一些更改,如果你是插件开发者也可以尝试发布新的插件版本,我在使用自己的版本号提取插件webpack-version-plugin时发现 compiler.plugin 已经被提示过气了, webpack@v4 使用最新的 compiler.hooks.emit.tap 触发事件,嗯,最后的这部分广告真硬!

23333

该文章首发于我的个人站点

查看原文

liuy666 收藏了文章 · 9月24日

Webpack 4 教程 - 4. 使用SplitChunksPlugin插件进行代码分割

Webpack 4给我们带来了一些改变。包括更快的打包速度,引入了SplitChunksPlugin插件来取代(之前版本里的)CommonsChunksPlugin插件。在这篇文章中,你将学习如何分割你的输出代码,从而提升我们应用的性能。

代码分割的理念

首先搞明白: webpack里的代码分割是个什么鬼? 它允许你将一个文件分割成多个文件。如果使用的好,它能大幅提升你的应用的性能。其原因是基于浏览器会缓存你的代码这一事实。每当你对某一文件做点改变,访问你站点的人们就要重新下载它。然而依赖却很少变动。如果你将(这些依赖)分离成单独的文件,访问者就无需多次重复下载它们了。

使用webpack生成一个或多个包含你源代码最终版本的“打包好的文件”(bundles),(概念上我们当作)它们由(一个一个的)chunks组成。

入口(Entry)

入口定义了我们的应用代码开始执行的那个文件,webpack从这个文件开始打包。你能定义一个入口点(常见于单页应用 - Single-Page Application), 或者多个入口点(常见于多页应用 - Multiple-Page Application)。

定义一个入口点就生成一个chunk。如果你只是用字符串的方式定义了一个入口点,其就被命名为main。如果你用对象的方式定义多个入口点,其就被命名为入口对象中的键值。下面两个例子是等价的:

entry: './src/index.js'
entry: {
  main: './src/index.js'
}

输出(Output)

输出对象配置webpack如何输出我们的打包(bundles)和资源(assets),以及将它们放到哪里。因为可能多于一个入口点,而只(能)指定一个输出配置。事实上我们就用chunks来给其一一命名。你能给打包输出的文件定义一个确定的名字,但既然我们想要分割我们的代码,就不能这么干。你得使用[name]来创建输出文件名的模板:

output: {
  filename: '[name].[chunkhash].bundle.js',
  path: path.resolve(__dirname, 'dist')
}

这里要注意的重要事情是 [chunkhash]: 它基于你文件的内容给每个chunk生成了一个特有的hash。它只有在你的文件内容本身变化的时候才变化。事实上,(如果内容没有变化)浏览器会缓存它。如果文件名改变了(译注:这里是指hash变化了,而hash是文件名的一部分,即意味着文件的内容变化了),浏览器就知道要重新下载了。chunkhash看起来长得就象这样子: 0c553ebfd158e16da428

如此这般,我们的main chunk就会被打包成名为 main.[chunkhash].bundle.js的文件。

SplitChunksPlugin插件

正是有了SplitChunksPlugin插件,你能在你的应用中移出一部分到单独的文件中。如果一个模块被多个chunks使用,(分割出它之后)就能很容易的在这些chunks之间共享。这正是webpack的默认行为。

// utilities/users.js

export default [
  { firstName: "Adam", age: 28 },
  { firstName: "Jane", age: 24 },
  { firstName: "Ben",  age: 31 },
  { firstName: "Lucy", age: 40 }
]
// a.js

import _ from 'lodash';
import users from './users';

const adam = _.find(users, { firstName: 'Adam' });
// b.js

import _ from 'lodash';
import users from './users';

const lucy = _.find(users, { firstName: 'Lucy' });
// webpack.config.js

module.exports = {
  entry: {
    a: "./src/a.js",
    b: "./src/b.js"
  },
  output: {
    filename: "[name].[chunkhash].bundle.js",
    path: __dirname + "/dist"
  }
};

运行之后,你会看到webpack创建了二个文件: a.[chunkhash].bundle.js和b.[chunkhash].bundle.js,每一个文件都包含了lodash库的一份拷贝: 这不太好! 我之前说过webpack的默认行为会给共享库创建分离的文件,但这涉及到异步chunks,即我们异步导入文件。我们在讨论懒加载的时候再来更多的覆盖这一主题。为了针对所有类型的chunks,我们需要稍微改改我们的webpack配置:

// webpack.config.js

module.exports = {
  entry: {
    a: "./src/a.js",
    b: "./src/b.js"
  },
  output: {
    filename: "[name].[chunkhash].bundle.js",
    path: __dirname + "/dist"
  },
  optimization: {
    splitChunks: {
      chunks: "all"
    }
  },
};

现在我们看到创建了一个附加的名叫vendors~a~b.[chunkhash].bundle.js的文件,其包含了Lodash库。事实上这全靠了配置中本身默认固有一个cacheGroups的配置项:

splitChunks: {
    chunks: "all",
    cacheGroups: {
      vendors: {
        test: /[\\/]node_modules[\\/]/,
        priority: -10
      },
      default: {
        minChunks: 2,
        priority: -20,
        reuseExistingChunk: true
      }
    }
  }

首先,vendors这一项指明了包括来自node_modules目录中的文件。其次default这一项表示默认的缓存组,包括其它共享模块。这里有一个小小的问题:发生了重复。a.[chunkhash].bundle.js和b.[chunkhash].bundle.js都包含了users.js的内容。这是因为,SplitChunksPlugin插件默认只分割超过30kb的文件。我们能很容易的更改这点:

// webpack.config.js

module.exports = {
  entry: {
    a: "./src/a.js",
    b: "./src/b.js"
  },
  output: {
    filename: "[name].[chunkhash].bundle.js",
    path: __dirname + "/dist"
  },
  optimization: {
    splitChunks: {
      chunks: "all",
      minSize: 0
    }
  }
};

默认的cache group配置使得会生成一个名为a~b.[chunkhash].bundle.js的文件。因为我们的users.js文件大大小于30kb,如果没有修改minSize属性的话,它就不会分割成一个单独的文件。在真实情形下,这是合理的,因为(如分割)并不能带来性能确实的提升,反而使得浏览器多了一次对utilities.js的请求,而这个utilities.js又是如此之小(不划算)。

我们能更进一小步,只针对utilities目录中的文件:

// webpack.config.js

const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: {
    a: "./src/a.js",
    b: "./src/b.js"
  },
  output: {
    filename: "[name].[chunkhash].bundle.js",
    path: __dirname + "/dist"
  },
  optimization: {
    splitChunks: {
      chunks: "all",
      cacheGroups: {
        utilities: {
          test: /[\\/]src[\\/]utilities[\\/]/,
          minSize: 0
        }
      }
    }
  }
};

现在我们打包出4个文件:a.[chunkhash].bundle.js, b.[chunkhash].bundle.js, vendors~a~b.[chunkhash].bundle.js 和 utilities~a~b.[chunkhash].bundle.js。即使我们现在设置一个全局的minSize: 0(在splitChunks对象中),默认的cache group也不会创建。这是因为被我们创建的utilities group覆盖了。utilities group默认的优先级值是0, 高于default cache group的。你可能已经注意到,默认cache group的优先级设置成了-20。

还有其它一些默认参数的设置,你能查阅SplitChunksPlugin的文档

小结

即使你只有一个入口点(见于大多数的单页应用),分离依赖到单独的文件中也是一个好主意。使用SplitChunksPlugin来达到这一目标实际上很简单,因为这是Webpack 4的默认行为,很有可能你只需设置chunks: “all”就足够了。如果你想要我说说其它有关的东西,告诉我吧。很快我们将学习如何应用懒加载来更好的提升性能, 敬请期待!

查看原文

liuy666 收藏了文章 · 9月24日

vue 圆形进度条组件解析

项目简介

  • 本组件是vue下的圆形进度条动画组件
  • 自由可定制,几乎全部参数均可设置
  • 源码简单清晰

    运行效果

面向人群

  • 急于使用vue圆形进度条动画组件的同学。直接下载文件,拷贝代码即可运行。
  • 喜欢看源码,希望了解组件背后原理的同学。
    刚接触前端的同学也可以通过本文章养成看源码的习惯。打破对源码的恐惧,相信自己,其实看源码并没有想象中的那么困难

    原来如此

组件使用方法及参数解析

 <circle-progress
   :id="1"
   :width="200"
   :radius="20"
   :progress="70"
   :delay="200"
   :duration="1000"
   :barColor="#F2AE57"
   :backgroundColor="#FFE8CC"
   :isAnimation="true"
   :timeFunction="cubic-bezier(0.99, 0.01, 0.22, 0.94)"
 >

| 参数名 | 值类型 | 是否必填 | 参数作用 | 默认值 |
| :------: | :------: | :------: | :------: | :------: |
| id | String | 选填 | 组件的id,多次定义设置不同的值 | 1 |
| width | Number | 必填 | 设置圆整体的大小,单位为px | 无 |
| radius | Number | 必填 | 设置进度条宽度,单位为px | 无 |
| progress | Number | 必填 | 设置进度百分比 | 无 |
| barColor | String | 必填 | 设置进度条颜色 | 无 |
| backgroundColor | String | 必填 | 设置进度条背景颜色 | 无 |
| delay | Number | 选填 | 延迟多久执行,单位为ms | 20 |
| duration | Number | 选填 | 动画整体时长,单位为ms | 1000 |
| timeFunction | String | 选填 | 动画缓动算法 | cubic-bezier(0.99, 0.01, 0.22, 0.94) |
| isAnimation | Boolean | 选填 | 是否以动画的方式呈现 | true |

原理解析

圆形的绘画
  • 使用的是svg技术进行绘画
  • 原理很简单,就是两个圆的折叠显示,这里重点讲的是svg标签各属性的意义
  • r:圆的半径
  • cy:圆点的 y 坐标
  • cx:圆点的 x 坐标
  • stroke:画笔颜色
  • stroke-width:画笔宽度
  • stroke-linecap:画笔结束方式,是圆形结束还是垂直结束
  • stroke-dasharray:需要点数字,如果只设置一个值,则仅生成一条线的虚线,从而实现画直线
  • stroke-dashoffset:定义虚线开始的地方,即虚线的位移。从而隐藏一部分虚线,实现显示弧线的效果。动画的原理也是利用该属性,控制隐藏的部分,实现进度条的增长
  • fill:填充的图案或者颜色,由于这里直接使用画笔描绘图形,所以用不上,为了覆盖其默认值black,设置为none

    <circle ref="$bar"
      :r="(width-radius)/2"
      :cy="width/2"
      :cx="width/2"
      :stroke="barColor"
      :stroke-width="radius"
      :stroke-linecap="isRound ? 'round' : 'square'"
      :stroke-dasharray="(width-radius)*3.14"
      :stroke-dashoffset="isAnimation ? (width-radius) * 3.14 : (width - radius) * 3.14 * (100 - progress) / 100"
      fill="none"
    />
动画原理
  • css3的animation动画。
  • 由于动画关键帧「keyframes」的定义需要根据外部传入的参数决定,不能预先写死。
  • 所以通过生成style节点的方式插入关键帧。
  • 在组件beforeDestroy时,将生成的style节点删除掉。方式是通过给style节点添加id属性进行定位。
  • 动画是通过修改stroke-dashoffset的值实现,设置不同的stroke-dashoffset值,可以控制圆弧隐藏的内容
  • 完全隐藏时,stroke-dashoffset值即圆形进度条的周长
  • stroke-dashoffset除了在节点属性中设置,也可以通过css样式设置

      @keyframes circle_progress_keyframes_name_1 {
        from {
          stroke-dashoffset: 565.2px;
        }
        to {
          stroke-dashoffset: 169.56px;
        }
      }
      .circle_progress_bar1 {
        animation: circle_progress_keyframes_name_1 1000ms 20ms cubic-bezier(0.99, 0.01, 0.22, 0.94) forwards;
      }

项目源码及示例

这波组件较为简单,貌似没什么可说的了,就这样吧

乔巴表情图

查看原文

认证与成就

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

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2018-04-27
个人主页被 133 人浏览