Tenadolanter

Tenadolanter 查看完整档案

上海编辑  |  填写毕业院校  |  填写所在公司/组织 www.kelede.win 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

Tenadolanter 赞了文章 · 2020-11-08

@vue/cli 项目编译重复命中缓存问题解析

@vue/cli 项目编译重复命中缓存问题解析

文章首发于个人blog,欢迎关注~

背景

最近遇到一个更新了 package,但是本地编译打包后没有更新代码的情况,先来复现下这个 case 的流程:

  1. A 同学在 npm 上发布了0.1.0版本的 package;
  2. B 同学开发了一个新的 feature,并发布0.2.0版本;
  3. C 同学将本地的0.1.0版本升级到0.2.0版本,并执行npm run deploy,代码经过 webpack 本地编译后发布到测试环境。但是测试环境的代码并不是最新的 package 的内容。但是在 node_modules 当中的 package 确实是最新的版本。

这个问题其实在社区里面有很多同学已经遇到了:

TL;DR(流程分析较复杂,可一拉到底)

发现 & 分析问题

翻了那些 issue 后,基本知道了是由于 webpack 在编译代码过程中走到 cache-loader 然后命中了缓存,这个缓存是之前编译的老代码,既然命中了缓存,那么就不会再去编译新的代码,于是最终编译出来的代码并不是我们所期望的。所以这个时候 cd node_modules && rm -rf .cache && npm run deploy,就是进入到 node_modules 目录,将 cache-loader 缓存的代码全部清除掉,并重新执行部署的命令,这些编译出来的代码肯定是最新的。

既然知道了问题的所在,那么就开始着手去分析这个问题的来龙去脉。这里我也简单的介绍下 cache-loader 的 workflow 是怎么进行的:

  1. 在 cache-loader 上部署了 pitch 方法(有关 loader pitch function 的用法可戳我),在 pitch 方法内部会根据生成的 cacheKey(例如abc) 去寻找 node_modules/.cache 文件夹下的缓存的 json 文件(abc.json)。其中 cacheKey 的生成支持外部传入 cacheIdentifier 和 cacheDirectory 具体参见官方文档
// cache-loader 内部定义的默认的 cacheIdentifier 及 cacheDirectory
const defaults = {
  cacheContext: '',
  cacheDirectory: findCacheDir({ name: 'cache-loader' }) || os.tmpdir(),
  cacheIdentifier: `cache-loader:${pkg.version} ${env}`,
  cacheKey,
  compare,
  precision: 0,
  read,
  readOnly: false,
  write
}

function cacheKey(options, request) {
  const { cacheIdentifier, cacheDirectory } = options;
  const hash = digest(`${cacheIdentifier}\n${request}`);

  return path.join(cacheDirectory, `${hash}.json`);
}

如果缓存文件(abc.json)当中记录的所有依赖以及这个文件都没发生变化,那么就会直接读取缓存当中的内容,并返回且跳过后面的 loader 的正常执行。一旦有依赖或者这个文件发生变化,那么就正常的走接下来的 loader 上部署的 pitch 方法,以及正常的 loader 处理文本文件的流程。

cache-loader 在决定是否使用缓存内容时是通过缓存内容当中记录的所有的依赖文件的 mtime 与对应文件最新的 mtime 做对比来看是否发生了变化,如果没有发生变化,即命中缓存,读取缓存内容并跳过后面的 loader 的处理,否则走正常的 loader 处理流程。

function pitch(remainingRequest, prevRequest, dataInput) {
  ...
  // 根据 cacheKey 的标识获取对应的缓存文件内容
  readFn(data.cacheKey, (readErr, cacheData) => {
    async.each(
      cacheData.dependencies.concat(cacheData.contextDependencies), // 遍历所有依赖文件路径
      (dep, eachCallback) => {
        // Applying reverse path transformation, in case they are relatives, when
        // reading from cache
        const contextDep = {
          ...dep,
          path: pathWithCacheContext(options.cacheContext, dep.path),
        };

        // fs.stat 获取对应文件状态
        FS.stat(contextDep.path, (statErr, stats) => {
          if (statErr) {
            eachCallback(statErr);
            return;
          }

          // When we are under a readOnly config on cache-loader
          // we don't want to emit any other error than a
          // file stat error
          if (readOnly) {
            eachCallback();
            return;
          }

          const compStats = stats;
          const compDep = contextDep;
          if (precision > 1) {
            ['atime', 'mtime', 'ctime', 'birthtime'].forEach((key) => {
              const msKey = `${key}Ms`;
              const ms = roundMs(stats[msKey], precision);

              compStats[msKey] = ms;
              compStats[key] = new Date(ms);
            });

            compDep.mtime = roundMs(dep.mtime, precision);
          }
          
          // 对比当前文件最新的 mtime 和缓存当中记录的 mtime 是否一致
          // If the compare function returns false
          // we not read from cache
          if (compareFn(compStats, compDep) !== true) {
            eachCallback(true);
            return;
          }
          eachCallback();
        });
      },
      (err) => {
        if (err) {
          data.startTime = Date.now();
          callback();
          return;
        }
        ...
        callback(null, ...cacheData.result);
      }
    );
  })
}
  1. 通过 @vue/cli 初始化的项目内部会通过脚手架去完成 webpack 相关的配置,其中针对 vue SFC 文件当中的script blocktemplate block在代码编译构建的流程当中都利用了 cache-loader 进行了缓存相关的配置工作。
// @vue/cli-plugin-babel
module.export = (api, options) => {
  ...
  api.chainWebpack(webpackConfig => {
    const jsRule = webpackConfig.module
      .rule('js')
        .test(/\.m?jsx?$/)
        .use('cache-loader')
          .loader(require.resolve('cache-loader'))
          .options(api.genCacheConfig('babel-loader', {
            '@babel/core': require('@babel/core/package.json').version,
            '@vue/babel-preset-app': require('@vue/babel-preset-app/package.json').version,
            'babel-loader': require('babel-loader/package.json').version,
            modern: !!process.env.VUE_CLI_MODERN_BUILD,
            browserslist: api.service.pkg.browserslist
          }, [
            'babel.config.js',
            '.browserslistrc'
          ]))
          .end()
    jsRule
      .use('babel-loader')
        .loader(require.resolve('babel-loader'))
  })
  ...
}

// @vue/cli-serive/lib/config
module.exports = (api, options) => {
  ...
  api.chainWebpack(webpackConfig => {
    const vueLoaderCacheConfig = api.genCacheConfig('vue-loader', {
      'vue-loader': require('vue-loader/package.json').version,
      /* eslint-disable-next-line node/no-extraneous-require */
      '@vue/component-compiler-utils': require('@vue/component-compiler-utils/package.json').version,
      'vue-template-compiler': require('vue-template-compiler/package.json').version
    })

    webpackConfig.module
      .rule('vue')
        .test(/\.vue$/)
        .use('cache-loader')
          .loader(require.resolve('cache-loader'))
          .options(vueLoaderCacheConfig)
          .end()
        .use('vue-loader')
          .loader(require.resolve('vue-loader'))
          .options(Object.assign({
            compilerOptions: {
              whitespace: 'condense'
            }
          }, vueLoaderCacheConfig))
    ...
  })
}

即:

  • 对于script block来说经过babel-loader的处理后经由cache-loader,若之前没有进行缓存过,那么新建本地的缓存 json 文件,若命中了缓存,那么直接读取经过babel-loader处理后的 js 代码;
  • 对于template block来说经过vue-loader转化成 renderFunction 后经由cache-loader,若之前没有进行缓存过,那么新建本地的缓存 json 文件,若命中了缓存,那么直接读取 json 文件当中缓存的 renderFunction。

上面对于 cache-loader 和 @vue/cli 内部工作原理的简单介绍。那么在文章一开始的时候提到的那个 case 具体是因为什么原因导致的呢?

事实上在npm 5.8+版本,npm 将发布的 package 当中包含的文件的 mtime 都统一置为了1985-10-26T08:15:00.000Z(可参见 issue-20439)

A 同学(npm版本为6.4.1)发布了0.1.0的版本后,C 同学安装了0.1.0版本,本地构建后生成缓存文件记录的文件 mtime 为1985-10-26T08:15:00.000Z。B 同学(npm版本为6.2.1)发布了0.2.0,C 同学安装0.2.0版本,本地开始构建,但是经由 cache-loader 的过程当中,cache-loader 通过对比缓存文件记录的依赖的 mtime 和新安装的 package 的文件的 mtime,但是发现都是1985-10-26T08:15:00.000Z,这样也就命中了缓存,即直接获取上一次缓存文件当中所包含的内容,而不会对新安装的 package 的文件进行编译。

针对这个问题,@vue/cli 在19年4月的3.7.0版本(具体代码变更的内容请戳我)当中也做了相关的修复性的工作,主要是将:package-lock.jsonyarn.lockpnpm-lock.yaml,这些做版本控制文件也加入到了 hash 生成的策略当中:

// @vue/cli-service/lib/PluginAPI.js

class PluginAPI {
  ...
  genCacheConfig(id, partialIdentifier, configFiles = []) {
    ...
    if (!Array.isArray(configFiles)) {
      configFiles = [configFiles]
    }
    configFiles = configFiles.concat([
      'package-lock.json',
      'yarn.lock',
      'pnpm-lock.yaml'
    ])

    const readConfig = file => {
      const absolutePath = this.resolve(file)
      if (!fs.existsSync(absolutePath)) {
        return
      }

      if (absolutePath.endsWith('.js')) {
        // should evaluate config scripts to reflect environment variable changes
        try {
          return JSON.stringify(require(absolutePath))
        } catch (e) {
          return fs.readFileSync(absolutePath, 'utf-8')
        }
      } else {
        // console.log('the absolute path is:', fs.readFileSync(absolutePath, 'utf-8'))
        return fs.readFileSync(absolutePath, 'utf-8')
      }
    }

    // 获取版本控制文件的文本内容
    for (const file of configFiles) {
      const content = readConfig(file)
      if (content) {
        variables.configFiles = content.replace(/\r\n?/g, '\n')
        break
      }
    }

    // 将带有版本控制文件的内容加入到 hash 算法当中,生成新的 cacheIdentifier
    // 并传入 cache-loader(缓存文件的 cacheKey 依据这个 cacheIdentifier 来生成,👆上文有说明)
    const cacheIdentifier = hash(variables)
    return { cacheDirectory, cacheIdentifier }
  }
}

这样来做的核心思想就是:当你升级了某个 package 后,相应的版本控制文件也会对应的更新(例如 package-lock.json),那么再一次进行编译流程时,所生成的缓存文件的 cacheKey 就会是最新的,因为也就不会命中缓存,还是走正常的全流程的编译,最终打包出来的代码也就是最新的。

不过这次升级后,还是有同学在社区反馈命中缓存,代码没有更新的问题,而且出现的 case 是 package 当中需要走 babel-loader 的 js 会遇到命中缓存不更新的情况,但是 package 当中被项目代码引用的 vue 的 template 文件不会出现这种情况。后来我调试了下@vue/cli-service/lib/PluginAPI.js的代码,发现代码在读取多个配置文件的过程中,一旦获取到某个配置文件的内容后就不再读取后面的配置文件的内容了,这样也就导致就算package-lock.json发生了更新,但是因为在编译流程当中并未读取package-lock.json这个文件的最新的内容话,那么也就不会生成新的 cacheKey,仍然会出现命中缓存的问题:

// 针对需要走 babel-loader 流程的配置文件为:
['babel.config.js', '.browserslistrc', 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml']
// 针对需要缓存的 vue template 的配置文件为:
['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml']

// @vue/cli-service/lib/PluginAPI.js
class PluginAPI {
  ...
  genCacheConfig(id, partialIdentifier, configFiles = []) {
    ...
    if (!Array.isArray(configFiles)) {
      configFiles = [configFiles]
    }
    configFiles = configFiles.concat([
      'package-lock.json',
      'yarn.lock',
      'pnpm-lock.yaml'
    ])

    const readConfig = file => {
      ...
    }

    // 一旦获取到某个配置文件的内容后,就直接跳出了 for ... of 的循环
    // 那么也就不会继续获取其他配置文件的内容,
    // 所以对于处理 js 文件的流程来说,因为读取了 babel.config.js 的内容,那么也就不会再去获取更新后的 packge-lock.json 文件内容
    // 但是对于处理 vue template 的流程来说,配置文件当中第一项就位 package-lock.json,这种情况下会获取最新的 package-lock.json 文件,所以对于 vue template 的不会出现升级了 package 内容,但是会因为命中缓存,导致编译代码不更新的情况。
    for (const file of configFiles) {
      const content = readConfig(file)
      if (content) {
        variables.configFiles = content.replace(/\r\n?/g, '\n')
        break
      }
    }

    const cacheIdentifier = hash(variables)
    return { cacheDirectory, cacheIdentifier }
  }
}

不过就在前几天,@vue/cli 的作者也重新看了下这个有关 vue template 正常,但是对于 js 命中缓存的原因,并针对这个问题进行了修复(具体代码内容变更请戳我),这次的代码变更就是通过 map 循环(而非 for ... of 循环读取到内容后直接 break),这样去确保所有的配置文件都被获取得到:

variables.configFiles = configFiles.map(file => {
  const content = readConfig(file)
  return content && content.replace(/\r\n?/g, '\n')
}

目前在@vue/cli-service@4.1.2版本中已经进行了修复。

以上就是通过 @vue/cli 初始化的项目,在升级 package 的过程中,cache-loader 命中缓存,新一轮代码编译生成非最新代码问题的分析。

总结 & 解决方案

cache-loader 使用缓存文件(node_modules/.cache)记录了不同依赖文件的 mtime,并通过对比缓存记录的 mtime 和最新文件的 mtime 是否发生了变化来觉得是否使用缓存。由于npm@5.8.0之后,每次新发布的 package 内部所包含的文件的 mtime 都被重置为1985-10-26T08:15:00.000Z,导致 cache-loader 这个对比 mtime 的策略失效。因为 @vue/cli-service 从3.7.0(19年4月)版本针对这个问题进行了第一次的修复,核心思想就是将package-lock.json这样的版本控制文件的内容纳入到了生成缓存文件的 cacheKey 的 hash 算法当中,每次升级 package 后,package-lock.json也会随之变化,这样会生成新的 cacheKey,进而不会命中缓存策略,这样也就解开了由于 npm 重置 mtime 而带来的重复命中缓存的问题,但是3.7.0版本的修复是有bug的,主要就是有些项目当中package-lock.json(由项目结构决定)这样的版本控制文件根本就没有被读取,导致 cache-loader 生成的 cacheKey 依然没有变化。然后在前几天(2020年1月28日),@vue/cli 的作者重新针对这个问题进行优化,确保package-lock.json版本控制文件能被读取到,从而避免 cacheKey 不变的问题,于@vue/cli-service@4.1.2版本中完全修复了重复命中缓存的问题。

这里比较有意思的一点就是这个问题的出现需要满足2个条件:

  1. 发布 package 的同学使用的 npm 的版本需要高于 5.8.0;
  2. 使用 package 的同学使用的 @vue/cli-service 的版本要低于 4.1.2 版本

比如我一直使用的 node 版本为 8.11.0,对应的 npm 版本为 5.6.0,那么经由我去修改发布的所有 package 所包含的文件的 mtime 都是被修改的那一刻,其他人升级到我发布的版本后,是不会出现重复命中缓存的问题。

不过既然问题被梳理清楚后,那么本地编译的过程避免出现这个问题的解决方式:

  1. 如果你的项目使用 @vue/cli@4.x 初始化的,那么直接升级 @vue/cli-service 到 4.1.2 版本即可;
  2. 如果你不想升级 @vue/cli-service 的版本(特别是你是使用 @vue/cli@3.x 版本初始化项目的同学,可能会出现兼容性问题,具体可自行测试),那么可以在每次本地编译开始前,删除掉node_module/.cache文件夹,例如将本地编译构建的npm script修改为rm -rf node_module/.cache && vue-cli-service build。(不过对于大型的项目来说,少了这部分的缓存内容的话,编译速度还是会受到一定的影响的。)
查看原文

赞 2 收藏 0 评论 0

Tenadolanter 赞了回答 · 2020-11-08

有办法给slot加事件吗

定义:

  <slot
            name="item"
            :item="item"
            :selectKeysObject="selectKeysObject"
            :onCheck="() => onSelect({ id: item[sourceId], item })"
          ></slot>

使用:

 <template #item="{ item, selectKeysObject, onCheck }">
          <div :class="$style.list">
            <div :class="$style.check" @click="onCheck">
              <simple-checkbox :checked="Boolean(selectKeysObject[item.sourceId])"></simple-checkbox>
            </div>
            <div :class="$style.img">
              <loading-image :url="item.sceneImg" />
            </div>
            <div>{{ item.absTimestamp }}</div>
            <div :class="$style.deviceName" :title="item.deviceName">{{ item.deviceName }}</div>
          </div>
        </template>

利用箭头函数,在使用的地方解析出来直接使用

微信图片_20200416173847.png

关注 6 回答 5

Tenadolanter 回答了问题 · 2020-07-27

解决关于Vue-cli3.0 的配置。pages.chunks的参数的意义

1、产生的效果

pages.chunks 取自webpack插件 HtmlWebpackPlugin 里面的 chunks 属性,在打包的时候会被插入到入口文件index.html里面,他大概长这样

<!DOCTYPE html>
<html lang="en">
  ...
  <body>
    <div id="app"></div>
    <!-- 注意下面这个家伙,就是它,文件名以chunk-vendors开头 -->
    <script type="text/javascript" data-original="/module_a/js/chunk-vendors.72b338bb.js"></script>
  </body>
</html>

2、从哪里看配置

如果你使用@vue/cli, 使用命令 vue inspect可以查看到配置里面有这么一段代码:

new HtmlWebpackPlugin(
  {
    title: 'test',
    templateParameters: function () { /* omitted long function */ },
    // 看他看他
    chunks: [
      'module_a',
      'chunk-vendors',
      'chunk-common'
    ],
    template: './public/index.html',
    filename: 'module_a.html',
    minify: {},
    inject: true
  }
)

3、为什么配置的是chunk-vendors和chunk-common,而不是其他?

继续使用 vue inspect, 可以看到有这样一段代码:

optimization: {
    splitChunks: {
      cacheGroups: {
        vendors: {
          name: 'chunk-vendors',
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
          chunks: 'initial'
        },
        common: {
          name: 'chunk-common',
          minChunks: 2,
          priority: -20,
          chunks: 'initial',
          reuseExistingChunk: true
        }
      }
    }
}

Vue对生产环境代码的打包进行了处理,对 node_modules 和使用超过两次的文件提取到公共部分,生成了名称以 chunk-vendorschunk-common 开头的文件,所以我们需要在模块入口文件里面引入这两个模块。

关注 3 回答 3

Tenadolanter 赞了文章 · 2020-05-27

简要总结microtask和macrotask

前言

我是在做前端面试题中看到了setTimeout和Promise的比较,然后第一次看到了microtask和macrotask的概念,在阅读了一些文章之后发现没有一个比较全面易懂的文章,所以我尝试做一个梳理性的总结.

这道经典的面试题引起了我的兴趣

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);

Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});

console.log('script end');

JavaScript的事件循环机制

首先我们先弄清楚setTimeout和Promise的共同点,也就是我第一次的看到那道面试题的疑惑点.

JavaScript 主线程拥有一个 执行栈 以及一个 任务队列,主线程会依次执行代码,当遇到函数时,会先将函数 入栈,函数运行完毕后再将该函数 出栈,直到所有代码执行完毕。

上面的例子的执行栈执行顺序应该是这样的

console.log('script start');
console.log('script end');
Promise.resolve();

而任务队列的执行顺序应该是这样的

Promise.then(function() {
  console.log('promise1');
});
Promise.then(function() {
  console.log('promise2');
});
setTimeout(function() {
  console.log('setTimeout');
}, 0);

而主线程则会在 清空当前执行栈后,按照先入先出的顺序读取任务队列里面的任务。

众所周知setTimeout和Promise.then()都属于上述异步任务的一种,那到底为什么setTimeout和Promise.then()会有顺序之分,这就是我想分析总结的问题所在了.

macrotasks(tasks) 和 microtasks

tasks

tasks的作用是为了让浏览器能够从内部获取javascript / dom的内容并确保执行栈能够顺序进行。

tasks的调度是随处可见的,例如解析HTML,获得鼠标点击的事件回调等等,在这个例子中,我们所迷惑的setTimeout也是一个tasks.

microtasks

microtasks通常用于在当前正在执行的脚本之后直接发生的事情,比如对一系列的行为做出反应,或者做出一些异步的任务,而不需要新建一个全新的tasks。

只要执行栈没有其他javascript在执行,在每个tasks结束时,microtasks队列就会在回调后处理。在microtasks期间排队的任何其他microtasks将被添加到这个队列的末尾并进行处理。

microtasks包括mutation observer callbacks,就像上例中的promise callbacks一样。

所以上面的例子执行顺序的实质是

  • tasks =>start end以及resolve
  • microtasks =>promise1和promise2
  • tasks =>setTimeout

具体应用

需要注意的是,在两个tasks之间,浏览器会重新渲染。这也是我们需要了解tasks和microtasks的一个非常重要的原因.

Vue 中如何使用 MutationObserver 做批量处理? - 顾轶灵的回答 - 知乎

根据 HTML Standard,在每个 task 运行完以后,UI 都会重渲染,那么在 microtask 中就完成数据更新,当前 task 结束就可以得到最新的 UI 了。反之如果新建一个 task 来做数据更新,那么渲染就会进行两次。

浏览器兼容问题

在__Microsoft Edge__, Firefox 40__, __iOS Safari 以及 desktop Safari 8.0.8 中setTimeout会先于Promise

该例子来自Jake Archibald-->Tasks, microtasks, queues and schedules,其中有动画来展现tasks和microtasks的具体工作流程,十分推荐阅读

//html
<div class="outer">
  <div class="inner"></div>
</div>
// Let's get hold of those elements
var outer = document.querySelector('.outer');
var inner = document.querySelector('.inner');

// Let's listen for attribute changes on the
// outer element
new MutationObserver(function() {
  console.log('mutate');
}).observe(outer, {
  attributes: true
});

// Here's a click listener…
function onClick() {
  console.log('click');

  setTimeout(function() {
    console.log('timeout');
  }, 0);

  Promise.resolve().then(function() {
    console.log('promise');
  });

  outer.setAttribute('data-random', Math.random());
}

// …which we'll attach to both elements
inner.addEventListener('click', onClick);
outer.addEventListener('click', onClick);

在这个例子中,不同浏览器的log是不同的,如下所示

ChromeFirefoxSafariedge
clickclickclickclick
promisemutatemutateclick
mutateclickclickmutate
clickmutatemutatetimeout
promisetimeoutpromisepromise
mutatepromisepromisetimeout
timeoutpromisetimeoutpromise
timeouttimeouttimeout\

事实上Chrome是正确的,而且由此可发现microtasks并不是在tasks的结束阶段开始执行,而是在tasks中回调结束之后(只要没有正在执行的JavaScript代码)

总结

tasks会顺序执行,浏览器会在执行间隔重新渲染

microtasks会顺序执行,执行时机为

在没有JavaScript代码执行的callback之后

在每一个tasks之后


由于我是前端初学者,对于JavaScript还很不熟悉,对事件循环的进程模型不是很了解,希望这篇文章能够帮助大家.

事件循环机制建议参考文章

阮一峰-->JavaScript 运行机制详解:再谈Event Loop

HTML Living Standard — Last Updated 9 April 2018

tasks建议参考文章

Jake Archibald-->Tasks, microtasks, queues and schedules

理解 JavaScript 中的 macrotask 和 microtask

setImmediate.js --A YuzuJS production

查看原文

赞 9 收藏 6 评论 0

Tenadolanter 赞了文章 · 2020-05-25

浅析toString与valueOf

valueOf():返回最适合该对象类型的原始值;
toString(): 将该对象的原始值以字符串形式返回。

这两个方法一般是交由JS去隐式调用,以满足不同的运算情况。
在数值运算里,会优先调用valueOf(),在字符串运算里,会优先调用toString()。

let e2 = {
        n : 2,
        toString : function (){
            console.log('this is toString')
            return this.n
        },
        valueOf : function(){
            console.log('this is valueOf')
            return this.n*2
        }
    }
    alert(e2) //  2  this is toString
    alert(+e2)  // 4 this is valueOf
    alert(''+e2) // 4 this is valueOf
    alert(String(e2)) // 2 this is toString
    alert(Number(e2)) // 4 this is valueOf
    alert(e2 == '4') // true  this is valueOf
    alert(e2 === 4) //false ===操作符不进行隐式转换

第三个alert,之所以会调用valueOf是因为:在有运算操作符的情况下,valueOf的优先级高于toString

看接下来的两给例子:

let e3 = {
        n : 2,
        toString : function (){
            console.log('this is toString')
            return this.n
        }
    }
    alert(e3) //  2  this is toString
    alert(+e3)  // 2 this is toString
    alert(''+e3) // 2 this is toString
    alert(String(e3)) // 2 this is toString
    alert(Number(e3)) // 2 this is toString
    alert(e3 == '2') // true  this is toString
    alert(e3 === 2) //false  ===操作符不进行隐式转换
    
    
   Object.prototype.toString = null; 
   let e4 = {
        n : 2,
        valueOf : function(){
            console.log('this is valueOf')
            return this.n*2
        }
    }
    alert(e4) //  4 this is valueOf
    alert(+e4)  // 4 this is valueOf
    alert(''+e4) // 4 this is valueOf
    alert(String(e4)) // 4 this is valueOf
    alert(Number(e4)) // 4 this is valueOf
    alert(e4 == '4') // true  this is valueOf
    alert(e4 === 4) //false  ===操作符不进行隐式转换
    
 

修改过的toString与ValueOf调用顺序

 哪个修改先调用哪个

总结:

  1. 进行对象转换时(alert(e2)),优先调用toString方法,如没有重写toString将调用valueOf方法,如果两方法都不没有重写,但按Object的toString输出。

  2. 进行强转字符串类型时将优先调用toString方法,强转为数字时优先调用valueOf。

  3. 在有运算操作符的情况下,valueOf的优先级高于toString。

查看原文

赞 7 收藏 6 评论 0

Tenadolanter 赞了文章 · 2020-05-23

chrome 开发者工具 - local overrides

使用chrome 作为本地网络服务

chrome 65+ 新功能, 使用我们自己的本地资源覆盖网页所使用的资源,可以使用本地css文件覆盖网页的css文件,修改样式。

类似的,使用DevTools的工作区设置持久化,将本地的文件夹映射到网络,在chrome开发者功能里面对css 样式的修改,都会直接改动本地文件,页面重新加载,使用的资源也是本地资源,达到持久化的效果。

然后就是,很少使用的使用 local override 功能,来搭建一个本地的静态网页服务器
搭建过程非常简单,根据原文中的步骤(假设访问的域名 chromeserver.com):

  1. 搭建local overrider的根目录, C:/Dev/Web/chromelocal,
  2. 在根目录中新建文件夹,以 chromeserver.com 命名,进入该文件目录,新建一个 index.html
  3. 打开chrome 开发者工具, sources --> Overrides --> 勾选 Enable Local Overriders --> 点击 Select folder for overrides ,选择文件 C:/Dev/Web/chromelocal
  4. 结果图 : overrides.png

在打开了 开发者工具的tab中,访问 http://chromeserver.com/,就可以看到页面了。

扩展:

1. 设置持久化。

经过上面的设置后,尝试打开其他网页, 在Elements 面板中进行的样式,

change-class

然后,在sources 中,就会自动生成对应域名的文件(在本地磁盘上新建文件)
图片描述

新开tab 中重新打开页面,修改依然生效,已经将本地的资源文件映射到了网络,跟上面工作区设置持久化的效果类似。

在chrome 中点击对应index.css文件,可以在右侧面板中,直接修改未格式化的文件
indexcss

或者在本地磁盘中,使用IDE打开文件 C:/Dev/Web/chromelocal/static.segmentfault.com/v-5badf55c/index/css/index.css,格式化文件后,再修改里面的内容,也可以自动刷新,显示文件更改。

2. 模拟接口数据

对于返回json 数据的接口,可以利用该功能,简单模拟返回数据。

比如:
api 为 http://www.xxx.com/api/v1/list,

对应在chromelocal 目录下,新建文件 www.xxx.com/api/v1/list,list 文件中的内容,与正常接口返回格式相同,
图片描述

对象或者数组类型,从而覆盖掉原接口请求。

相关功能

workspaces ,chrome 很早就引入了 workspaces , 这个功能允许把DevTools 当做IDE使用,
只要在选择了 Add folder to workspace 后,允许资源访问,在chrome DevTools 中的改变,相应也会保存在本地版本中,跟使用文本编辑器类似的类似的功能。

在 Chrome 63 What's New In DevTools中 提及 workspace2.0 发布,增强用户体验,不过延期了,

查看原文

赞 18 收藏 11 评论 2

Tenadolanter 回答了问题 · 2020-05-20

有办法查看github上已经删除的分支吗

刚找回一个很久之前被删除的分支,也是因为幸运。

之前尝试使用git reflog命令,但是很不幸没找到这个分支对应的提交记录。

最后,是在sourceTree的远端origin里面找到了想要恢复的分支feature/recover_branch,最后在跳转到里面找到分支对应的commit id例如8888d814e

FCAD9952-845F-4EC2-8983-1591FC52751E.png

执行命令,恢复删除的分支,并推送到线上

# 根据commit id,本地新建分支
git checkout -b feature/recover_branch 8888d814e 

# 推送到远端
git push origin

关注 6 回答 2

Tenadolanter 赞了问题 · 2020-05-20

有办法查看github上已经删除的分支吗

比如通过git push origin :a_branch 删除的分支,或者在github网页上删除的branch,有办法查看删除branch的历史吗

关注 6 回答 2

Tenadolanter 回答了问题 · 2020-05-13

解决有没有办法主动触发window的onresize?

手动触发方法:

const myEvent = new Event('resize')
window.dispatchEvent(myEvent)

关注 10 回答 6

Tenadolanter 赞了文章 · 2020-05-08

html-webpack-plugin用法全解

本文只在个人博客和 SegmentFault 社区个人专栏发表,转载请注明出处
个人博客: https://zengxiaotao.github.io
SegmentFault 个人专栏: https://segmentfault.com/blog...

html-webpack-plugin 可能用过的 webpack 的童鞋都用过这个 plugin ,就算没用过可能也听过。我们在学习webpack的时候,可能经常会看到这样的一段代码。

// webpack.config.js
module.exports = {
    entry: path.resolve(__dirname, './app/index.js'),
    output:{
        path: path.resolve(__dirname, './build'),
        filename: 'bundle.js'
    }
    ...
    plugins: [
        new HtmlWebpackPlugin()
    ]
}

之后在终端输入 webpack 命令后

webpack

你会神奇的看到在你的 build 文件夹会生成一个 index.html 文件和一个 bundle.js 文件,而且 index.html 文件中自动引用 webpack 生成的 bundle.js 文件。

所有的这些都是 html-webpack-plugin 的功劳。它会自动帮你生成一个 html 文件,并且引用相关的 assets 文件(如 css, js)。

自己在六月第一次接触前端自动化构建,学习 webpack 和 react 时,曾经简单使用过这个插件,但也只是用了常见的几个选项,今天就跟着官方文档走一走,看看它的所有用法。

title

顾名思义,设置生成的 html 文件的标题。

filename

也没什么说的,生成 html 文件的文件名。默认为 index.html.

template

根据自己的指定的模板文件来生成特定的 html 文件。这里的模板类型可以是任意你喜欢的模板,可以是 html, jade, ejs, hbs, 等等,但是要注意的是,使用自定义的模板文件时,需要提前安装对应的 loader, 否则webpack不能正确解析。以 jade 为例。

npm install jade-loader --save-dev
// webpack.config.js
...
loaders: {
    ...
    {
        test: /\.jade$/,
        loader: 'jade'
    }
}
plugins: [
    new HtmlWebpackPlugin({
        ...
        jade: 'path/to/yourfile.jade'
    })
]

最终在build文件夹内会生成一个 yourfile.html 和 bundle.js 文件。现在我们再回头来看看之前将的 title 属性。

如果你既指定了 template 选项,又指定了 title 选项,那么webpack 会选择哪一个? 事实上,这时候会选择你指定的模板文件的 title, 即使你的模板文件中未设置 title

那么 filename 呢,是否也会覆盖,其实不是,会以指定的 filename 作为文件名。

inject

注入选项。有四个选项值 true, body, head, false.

  • true

    • 默认值,script标签位于html文件的 body 底部

  • body

    • 同 true

  • head

    • script 标签位于 head 标签内

  • false

    • 不插入生成的 js 文件,只是单纯的生成一个 html 文件

favicon

给生成的 html 文件生成一个 favicon。属性值为 favicon 文件所在的路径名。

// webpack.config.js
...
plugins: [
    new HtmlWebpackPlugin({
        ...
        favicon: 'path/to/yourfile.ico'
    }) 
]

生成的 html 标签中会包含这样一个 link 标签

<link rel="shortcut icon" href="example.ico">

同 title 和 filename 一样,如果在模板文件指定了 favicon,会忽略该属性。

minify

minify 的作用是对 html 文件进行压缩,minify 的属性值是一个压缩选项或者 false 。默认值为false, 不对生成的 html 文件进行压缩。来看看这个压缩选项。

html-webpack-plugin 内部集成了 html-minifier ,这个压缩选项同 html-minify 的压缩选项完全一样,
看一个简单的例子。

// webpack.config.js
...
plugins: [
    new HtmlWebpackPlugin({
        ...
        minify: {
            removeAttributeQuotes: true // 移除属性的引号
        }
    })
]
<!-- 原html片段 -->
<div id="example" class="example">test minify</div>
<!-- 生成的html片段 -->
<div id=example class=example>test minify</div>

hash

hash选项的作用是 给生成的 js 文件一个独特的 hash 值,该 hash 值是该次 webpack 编译的 hash 值。默认值为 false 。同样看一个例子。

// webpack.config.js
plugins: [
    new HtmlWebpackPlugin({
        ...
        hash: true
    })
]
<script type=text/javascript data-original=bundle.js?22b9692e22e7be37b57e></script>

执行 webpack 命令后,你会看到你的生成的 html 文件的 script 标签内引用的 js 文件,是不是有点变化了。
bundle.js 文件后跟的一串 hash 值就是此次 webpack 编译对应的 hash 值。

$ webpack
Hash: 22b9692e22e7be37b57e
Version: webpack 1.13.2

cache

默认值是 true。表示只有在内容变化时才生成一个新的文件。

showErrors

showErrors 的作用是,如果 webpack 编译出现错误,webpack会将错误信息包裹在一个 pre 标签内,属性的默认值为 true ,也就是显示错误信息。

chunks

chunks 选项的作用主要是针对多入口(entry)文件。当你有多个入口文件的时候,对应就会生成多个编译后的 js 文件。那么 chunks 选项就可以决定是否都使用这些生成的 js 文件。

chunks 默认会在生成的 html 文件中引用所有的 js 文件,当然你也可以指定引入哪些特定的文件。

看一个小例子。

// webpack.config.js
entry: {
    index: path.resolve(__dirname, './src/index.js'),
    index1: path.resolve(__dirname, './src/index1.js'),
    index2: path.resolve(__dirname, './src/index2.js')
}
...
plugins: [
    new HtmlWebpackPlugin({
        ...
        chunks: ['index','index2']
    })
]

执行 webpack 命令之后,你会看到生成的 index.html 文件中,只引用了 index.js 和 index2.js

...
<script type=text/javascript data-original=index.js></script>
<script type=text/javascript data-original=index2.js></script>

而如果没有指定 chunks 选项,默认会全部引用。

excludeChunks

弄懂了 chunks 之后,excludeChunks 选项也就好理解了,跟 chunks 是相反的,排除掉某些 js 文件。 比如上面的例子,其实等价于下面这一行

...
excludeChunks: ['index1.js']

chunksSortMode

这个选项决定了 script 标签的引用顺序。默认有四个选项,'none', 'auto', 'dependency', '{function}'。

  • 'dependency' 不用说,按照不同文件的依赖关系来排序。

  • 'auto' 默认值,插件的内置的排序方式,具体顺序这里我也不太清楚...

  • 'none' 无序? 不太清楚...

  • {function} 提供一个函数?但是函数的参数又是什么? 不太清楚...

如果有使用过这个选项或者知道其具体含义的同学,还请告知一下。。。

xhtml

一个布尔值,默认值是 false ,如果为 true ,则以兼容 xhtml 的模式引用文件。

总结

以上,就总结完了传入 new HtmlWebpackPlugin() 的选项,了解全部选项的含义后,可以在项目构建时更灵活的使用。

全文完

查看原文

赞 87 收藏 175 评论 24

Tenadolanter 赞了回答 · 2020-05-08

解决关于Vue-cli3.0 的配置。pages.chunks的参数的意义

找到问题了。

vue-cli3里的vue.config.js中的pages参数是会编译到webpack中的html-webpack-plugin的配置里。
所以vue.config.js中的pages.chunks也就等同于html-webpack-plugin中的chunks。所以参数的意义可以参考这个https://segmentfault.com/a/1190000007294861#articleHeader9

可以用vue-cli-service inspect命令打印出来的webpack配置来检查效果

另外。vue.config.js中的pages也不止官方给的那几个参数:entry, template, filename, title 和 chunks,也可以给pageshtml-webpack-plugin能接受的参数,比如favicon(亲测可以,其他参数懒得去尝试)

再另外。跑本地服务器(vue-cli-service serve)时是不会插入chunk-vendorschunk-common的。要打生产包(vue-cli-service build)然后看入口index.html才能看到html-webpack-plugin插入了chunk-vendorschunk-common

举个例子:

// vue.config.js
const businessArray = [
    {chunk: 'note', chunkName: '短信通知服务'},
    {chunk: 'evaluate', chunkName: '风险评估'}
]

let pages = {}

businessArray.forEach(v => {
    pages[v.chunk] = {
        // page 的入口
        entry: `src/business/${v.chunk}/index.js`,
        // 模板来源
        template: 'public/index.html',
        // 在 dist/index.html 的输出
        filename: `business/${v.chunk}/index.html`,
        // 当使用 title 选项时,
        // template 中的 title 标签需要是 <title><%= htmlWebpackPlugin.options.title %></title>
        title: v.chunkName,
        // 在这个页面中包含的块,默认情况下会包含
        // 提取出来的通用 chunk 和 vendor chunk。
        chunks: ['chunk-vendors', `${v.chunk}`],
        favicon: 'public/favicon.ico'
    }
})

module.exports = {
    pages
}

编译过后的webpack配置为

vue-cli-service inspect > webpack.js
// webpack.js
new HtmlWebpackPlugin(
      {
        templateParameters: function () { /* omitted long function */ },
        chunks: [
          'chunk-vendors',
          'note'
        ],
        template: 'public/index.html',
        filename: 'business/note/index.html',
        title: '短信通知服务',
        favicon: 'public/favicon.ico'
      }
    ),
    /* config.plugin('html-evaluate') */
    new HtmlWebpackPlugin(
      {
        templateParameters: function () { /* omitted long function */ },
        chunks: [
          'chunk-vendors',
          'evaluate'
        ],
        template: 'public/index.html',
        filename: 'business/evaluate/index.html',
        title: '风险评估',
        favicon: 'public/favicon.ico'
      }
    ),

关注 3 回答 3

Tenadolanter 赞了问题 · 2020-03-26

element的loading动画出现的很慢?

使用element的表格,加上loading组件。loading前有明显只有表格框架的时期,有什么办法加快loading出现的时间么?

clipboard.png

关注 4 回答 2

Tenadolanter 赞了文章 · 2020-02-14

懒人神器:svg-sprite-loader实现自己的Icon组件

用 svg-sprite-loader 解放你的icon.

好吧,这篇文章的起源就来源于——我懒。

UI小姐姐设计了自己的icon,但是我不想每次引入icon的时候都写一大堆:

<img data-original="/long/path/to/your/svg/icon.svg" />

很长很长的地址…我觉得最简单的形式还是像饿了么那些UI库一样,直接:

<el-icon name="icon-file-name"></el-icon>

写个文件名就能引入我的icon了。

OK, 以上就是我们的理想模式。So, let’s go!

工作原理

网上搜寻了一圈,一个简单的解决方案是 —— svg 雪碧图。

它的工作原理是: 利用svg的symbol元素,将每个icon包括在symbol中,通过use元素使用该symbol.

OK,如果你对此不了解,可以阅读张鑫旭老师的这篇文章.

我们这里简单一点的解释就是,最终你的svg icon会变成下面这个样子的 svg 雪碧图:

<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="__SVG_SPRITE_NODE__">
    <symbol class="icon" viewBox="0 0 1024 1024" id="icon名">{{省略的icon path}}</symbol>
    <symbol class="icon" viewBox="0 0 1024 1024" id="icon名">{{省略的icon path}}</symbol>
</svg>

你的每一个icon都对应着一个symbol元素。然后在你的html中,引入这样的svg, 随后通过use在任何你需要icon的地方指向symbol:

<use xlink:href="#symbolId"></use>

这个过程中,我们可以把symbol理解为sketch中内置的图形,当你需要使用的时候,把这个形状”拖拽”到你的画板中就行了。而use就是这个过程中的”拖拽”行为。

工具

要让我们自己生成上面那样的svg雪碧图——肯定是不可能的咯!
恩,你一定想到了,肯定有工具!当然你最常用的应该是webpack的工具吧,这里拿好!

svg-sprite-loader

svg-sprite-loader会把你的icon塞到一个个symbol中,symbol的id如果不特别指定,就是你的文件名。它最终会在你的html中嵌入这样一个svg
你就可以像上面这样:

<use xlink:href="#symbolId"></use>

随意使用你的icon咯。

svg-sprite-loader配置如下:

{
  test: /\.svg$/,
  loader: 'svg-sprite-loader',
}

有一点需要注意的是,我们并不是所有的svg都要放在我们的雪碧图里,有的也许我就想当做图片用。这时候在我们的webpack配置中,我们需要对这两种svg区别对待。
首先,我们要把所有要作为icon的svg团结在一起,放在某个文件夹中,例如assets/icons。其他的svg就随你便啦。

然后对于想要用作图片的:

{
  test: /\.svg$/,
  loader: 'file-loader',
  exclude: path.resolve(__dirname, './src/assets/icons') // 不带icon 玩
}

对于用作icon的:

{
  test: /\.svg$/,
  loader: 'svg-sprite-loader',
  include: path.resolve(__dirname, './src/assets/icons') // 只带自己人玩
}

最后,这俩就分道扬镳啦。

组件化

OK, 我们的问题已经解决了一半,不用每次都写路径引入svg文件了。
但是。。。我们现在要每次都写

<svg>
    <use xlink:href="#symbolId"></use>
</svg>

我!不!干!!!而且也没达到我们最初的目的。
所以,我们肯定把上面的那一坨写成一个组件咯:

<template>
  <svg :class="svgClass">
    <use :xlink:href="`#${name}`"></use>
  </svg>
</template>

<script>
  export default {
    name: 'icon',
    props: {
      name: {
        type: String,
        required: true,
      },
    },
  }
</script>

最后,你就达成目标,这样使用:

import 'your-icon.svg';
<icon name="your-icon-name"></icon>

如果你想修改图标的颜色,直接设置该元素的fill/stroke属性。如果设置了这些属性没有反应的话,emmm...可能需要你的设计师重新切图,同样是张鑫旭大佬
关于切图的这篇文章

引入所有Icon文件

上面我们的基本功能已经完成了,还有最后一个小小的问题——我每次引用一个文件的时候就得import一下,这肯定也不满足我们偷懒的最终目标。
不过,总会有人比你更懒,或者总会有人比你先懒。在这里,我们可以使用webpack的require.contextAPI来动态引入你所有的Icon.

现在我们是不能动态引入模块,但是webpack为我们提供了相关功能,webpack) 允许我们使用表达式动态引入模块。比如:require('./template/' + name + '.ejs');,此时webpack会生成一个context module

A context module is generated. It contains references to all modules in that directory that can be required with a request matching the regular expression. The context module contains a map which translates requests to module ids.

它会被抽象成以下信息:

{
  "./table.ejs": 42, // key 是module, value 是module id
  "./table-row.ejs": 43,
  "./directory/folder.ejs": 44
}

因此,我们可以利用webpack提供的的require.contextAPI 来创建自己的context module动态引入icon。它接受三个参数,第一个是文件夹,第二个是是否使用子文件,第三个是文件匹配的正则。
require.context(directory, useSubdirectories = false, regExp = /^\.\//)
对于我们的项目来说,我们需要动态引入的就是require.context('./src/assets/icons', false, /\.svg/).

require.context会返回一个函数,并且该函数有keys()idresolve() 属性。

  • keys()方法返回的该模块可以处理的所有可能请求的模块的数组,简单一点就是满足该参数的模块;
  • resolve()返回的是请求的module的id;
  • id是该context module的id;

总的来说,就是说require.context帮我们创建一个上下文,比如在这里我们的上下文就是./src/assets/icons, 随后我们就可以通过request.resolve('./store.svg')来引入该上下文内的文件了。

我们打印一下:

const request = require.context('./assets/icons', false, /\.svg$/);
console.log(request);
console.log(request.keys());
console.log(request.id);
console.log('request.resolve()', request.resolve('./store.svg'));
console.log(request.resolve);

得到的结果是:

// request
webpackContext(req) {
    var id = webpackContextResolve(req);
    return __webpack_require__(id);
}

// request.keys()
["./airbloom.svg", "./crown.svg", "./store.svg"]

// request.id
./src/assets/icons sync \.svg$

// request.resolve('./store.svg');
./src/assets/icons/store.svg

// request.resolve
webpackContextResolve(req) {
    var id = map[req];
    if(!(id + 1)) { // check for number or string
        var e = new Error("Cannot find module '" + req + "'");
        e.code = 'MODULE_NOT_FOUND';
        throw e;
    }

有关的源码在这里:

var map = {
    "./airbloom.svg": "./src/assets/icons/airbloom.svg",
    "./crown.svg": "./src/assets/icons/crown.svg",
    "./store.svg": "./src/assets/icons/store.svg"
};


function webpackContext(req) {
    var id = webpackContextResolve(req);
    return __webpack_require__(id);
}
function webpackContextResolve(req) {
    var id = map[req];
    if(!(id + 1)) { // check for number or string
        var e = new Error("Cannot find module '" + req + "'");
        e.code = 'MODULE_NOT_FOUND';
        throw e;
    }
    return id;
}
webpackContext.keys = function webpackContextKeys() {
    return Object.keys(map);
};
webpackContext.resolve = webpackContextResolve;
module.exports = webpackContext;
webpackContext.id = "./src/assets/icons sync \\.svg$";

最后,我们requestcontext module下的每一个module,引入我们所有的icon

// 由于request返回了一个函数,该函数接收req作为参数,在这里其实我们就是把request.keys()中的每一个module传入了request的返回函数中了
request.keys().forEach(request);

总结

  • 原理:

    • symbol + use:xlink:href;
    • svg-sprite-loader生成雪碧图;
    • require.context动态引入所有文件;
  • 优化SVG

有时候,设计师切的icon并不那么geek, 有很多多余的东西,可以使用大名鼎鼎的svgo进行优化,
它提供web在线版,webpack loader等。

  • 其他工具

vue-svgicon这款工具相比我们的有更多的feature,比如动画、方向等。它会给每个icon生成一个相对应的js文件,
用来注册这个icon。就我目前的应用场景来说,1. 它会生成很多js文件;2.每次新增一个svg时我就得run一次注册组件的命令。对于我现在的简单应用场景来说,并没有自己写的简单方便。
不过在其他的时候,他也可以作为另一个选择。

require.contextAPI.

参考资料

查看原文

赞 43 收藏 27 评论 3

Tenadolanter 赞了回答 · 2020-01-15

写了这样一段代码,执行结果和预期不一样,为什么?

ECMAScript中所有的函数中的参数是按值传递的,函数参数传递的并不是变量的引用,而是变量的拷贝副本。
当函数参数是对象时(即引用类型,并不是单纯的一个值,而是指向该对象的堆内存中的地址而已),所以此时函数内部对对象的属性进行操作,实际上和外部变量指向堆内存中的值是相同。举个例子
let A = { a : 1, b : 2 };
let B = A
B.a = 3
那么 A.a 也等于 3
有兴趣的话楼主你可以搜索 栈内存与堆内存

关注 5 回答 5

Tenadolanter 提出了问题 · 2020-01-15

写了这样一段代码,执行结果和预期不一样,为什么?

代码如下:

const testFunc = (str)=>{
    str = 'my test str'
}
let str = ''
testFunc(str)
console.log(str)

2020年01月15日23:18:16晚上新加:

一时迷糊了,因为经常直接在函数中操作对象,当换成字符串,发现不起作用后没有反应过来。如下代码:

let a = { name: '11111', sex: 'nan' }
const setName = (param) => {
    console.log(param === a)
    param.name = 'test'
}
setName(a);
console.log(a)

如果把上面的setName(a)换成setName(a.name)、把param.name = 'test'换成param = 'test',可以看到结果又不一样了。

出现上面结果的原因是没有完全理解全局作用域、函数作用域

  • 当传入参数是常量的时候,对参数的更改是在函数作用域里面,并不会影响全局作用域的值。
  • 当传入的参数是对象的时候,虽然函数外部是全局作用域,参数是函数作用域,但是由于对象使用的是同一块内存,当内容改变后,全局作用域里面的值和函数作用域里面的值都发生改变。

关注 5 回答 5

Tenadolanter 赞了问题 · 2020-01-09

解决使用translateZ(0)提升性能的原理是什么?

很多文章中都写到使用tanslateZ可以利用gpu加速,提高浏览器渲染的性能;一些文章中说原理是因为使用translateZ的元素会单独占据一个渲染层;这样减小了元素变动时,浏览器的重新绘制的区域。

但是实际上使用translate和opacity的元素就会单独占据一个渲染层,所以使用translateZ做硬件加速的真实原理是什么?

关注 7 回答 5

Tenadolanter 赞了回答 · 2020-01-09

解决关于点击空白关闭弹窗的js写法推荐?

来一个javascript 实现的方案吧 :

    // 点击其他区域时, 隐藏指定区域(cDom)
    document.addEventListener("click", event => {
      var cDom = document.querySelector("#filter-header");
      var tDom = event.target;
      if (cDom == tDom || cDom.contains(tDom)) {
        // ... 
      } else {
        cDom.style.display = "none"
      }
    });

关注 32 回答 12

Tenadolanter 赞了问题 · 2020-01-07

关于cookie的安全性问题

最近在学习cookie和session的基础知识,在这过程中涉及到cookie的安全性问题时总是很疑惑,很多文章都会涉及到安全性的问题,但是总是一句话带过,并没有详细说到底是怎么回事,比如我看到别人的观点:

服务器鉴别session需要至少从客户端传来一个session_id,session_id通常存于cookie中,或是url(很少用url,主要涉及安全性和SEO的影响)

所以在工程上session离了cookie基本没法用,但是cookie可以单独使用,不过cookies是明文存储,安全性很低,只使用cookie的话盗取了cookie基本就获取了用户所有权限。

亦或者

某个用户竟然在凭证上伪造了内容,让服务器哥误认为他是某个用户(这个时候凭证是明文,该起来非常方便),然后被伪造的那个用户的信息泄漏了

当我看到这些的时候总是不太清楚到底是怎么回事,以上面的例子为例,假如有人可以伪造cookie的内容那就算使用session,也可以有人伪造session_id啊,所以很疑惑。。

另外,大家可以具体讲下用户填写账号密码并提交表单后,服务器验证的具体过程吗?(是根据session_id从内存或者数据库中取出账号密码进行对比?)

感激不尽~

关注 20 回答 9

Tenadolanter 赞了文章 · 2019-12-29

「圣诞特辑」纯前端实现人脸识别自动佩戴圣诞帽

在线体验地址:hksite.cn/prjs/christmashat

源码地址:https://github.com/hk029/christmas-hat

写在开头

叮叮当,叮叮当,吊儿个郎当,一年一度的圣诞节到咯,我不由的回想起了前两年票圈被圣诞帽支配的恐惧。打开票圈全是各种@官方求帽子的:

Untitled/Untitled.png?imageView&thumbnail=400x400

票圈头像也瞬间被圣诞帽攻陷:

Untitled/Untitled%201.png?imageView&thumbnail=400x400

在那段时间,你没一顶圣诞帽还真不好意思发票圈

Untitled/Untitled%202.png?imageView&thumbnail=400x400

各种戴帽子的软件也如雨后春笋般浮现出来,不管是小程序还是美图软件无一例外的都增加了戴圣诞帽的功能。但是对于懒人的我来说,自己调整一个圣诞帽子佩戴还是太麻烦了。于是我就想了,有没有什么办法能让我的头像自动佩戴上圣诞帽呢?

还真给我想到了,这就是今天的主题,用纯前端的方式给你做一个自动佩戴圣诞帽的网站。

有了这个网站,你就能顺利在票圈装 13 了,不仅如此,你还可能因此邂逅一段完美的爱情!试想一下,当你发了这个网站在票圈后,女神看到了就会为你的技术所折服,然后主动把照片给你,让你帮她给头像戴上圣诞帽,然后你就顺利的得到了和女神搭讪的机会,然后赢取白富美,走向人生巅峰,想想还有点小激动呢。

Untitled/Untitled%203.png?imageView&thumbnail=400x400

给头像戴上圣诞帽需要几步

给头像佩戴上圣诞帽需要几个步骤呢?很简单,跟大象装进冰箱一样,只需要三个步骤:

  • 打开头像
  • 戴上圣诞帽
  • 下载图片

Untitled/Untitled%204.png?imageView&thumbnail=400x400

其中第一步和最后一步看起来好像都不是什么难事,关键是这第二点,如何给头像戴上圣诞帽?

首先你必须要懂的,当我在说:戴上圣诞帽的时候,我在说什么?让我来翻译以下:

将圣诞帽的图片素材绘制在头像图片的合适位置,使之变成一张图片

所以我们今天的重点来了:如何能确定圣诞帽的位置,并将它和头像合成为一张图片。

首先让我们来聊聊如何确定圣诞帽的位置。

确定圣诞帽的位置

通过手动的方式,我们是很容易确定圣诞帽应该放在什么位置的,那机器如何能确定呢?有人可能想到了那不就是人脸识别技术?是的,这就是我们今天需要用到的技术。

早在 2017 年之前,纯前端说想实现人脸识别还有点天方夜谭的感觉,但是 Tensorflow.js 的出现让这一切成为了可能:

Untitled/Untitled%205.png?imageView&thumbnail=400x400

它是 Google 推出的第一个基于 TensorFlow 的前端深度学习框架。它允许你在浏览器上训练模型,或以推断模式运行预训练的模型。TensorFlow.js 不仅可以提供低级的机器学习构建模块,还可以提供高级的类似 Keras 的 API 来构建神经网络。

Tensorflow.js 是我第一个想到的可以应用的库,但是当我打开官方文档,看到如 Tensors (张量)、Layers (图层)、Optimizers (优化器)……各种陌生概念扑面而来,砸的人生疼,现学感觉是来不及了,那有什么办法能在我不了解各种概念的情况下也能快速上手人脸识别呢?

答案当然有,那就是:face-api.js

face-api.js

face-api.js 是大神 Vincent Mühler 的最新力作,他为人所知的开源项目还有 opencv4nodejsface-recognize(NodeJs 的人脸识别包,不过现在 face-api.js 已经支持 Node 端了,他推荐直接使用 face-api)

face-api.js 是一个建立在 Tensorflow.js 内核上的 Javascript 模块,它实现了三种卷积神经网络(CNN)架构,用于完成人脸检测、识别和特征点检测任务。简而言之,借助它,前端也能很轻松的完成人脸识别的工作。

原理简析

想看实现的童鞋请直接略过这一段,直接开始上手操作。

我们知道机器学习有几个基本要素:数据,模型,算法。他们之间的关系如下:

Untitled/Untitled%206.png?imageView&thumbnail=400x400

  • 训练数据: 训练数据就是一系列打过标签的数据,比如一系列人脸和不是人脸的图片数据。
  • 模型(这里我们主要指监督学习模型): 模型你可以简单理解为是一个预测函数(f(x) = y),简单来说就是根据输入的数据,能给出结果。
  • 算法: 算法就是教机器如何获得最优的模型(损失最小)。比如当机器通过当前模型识别到一张训练图片为人脸,但是标签是「非人脸」,此时就需要根据算法对模型进行调整。常见的算法有例如:梯度下降法(Gradient Descent),共轭梯度法(Conjugate Gradient),牛顿法和拟牛顿法,模拟退火法(Simulated Annealing)……

所以,我们可以这么说,只要有了一个训练好的预测模型,我们就可以对未知数据进行分析预测了。

face-api 的原理

首先,为了在图片中识别出人脸,我们需要告诉机器什么样的脸是人脸,因此我们需要大量的人脸照片,并且标明里面的各种脸部特征数据,让机器去学习:

Untitled/Untitled%207.png?imageView&thumbnail=400x400

face-api.js 针对人脸检测工作实现了一个 SSD(Single Shot Multibox Detector)算法,它本质上是一个基于 MobileNetV1 的卷积神经网络(CNN),同时在网络的顶层加入了一些人脸边框预测层。

然后 face-api.js 会通过该算法让机器不断的学习并优化,从而训练出模型,通过该模型可以识别出所有的人脸边界框

光识别出人脸还远远不够,我们的很多应用都需要找到人脸的特征点(眉毛,鼻子,嘴巴这些的)。因此 face-api.js 会从图片中抽取出每个边界框中的人脸居中的图像,接着将其再次作为输入传给人脸识别网络,让其学习。

为了实现特征点识别这个目标,face-api.js 又实现了一个简单的卷积神经网络,它可以返回给定图像的 68 个人脸特征点:

通过该算法,face-api.js 训练了一系列的模型,通过使用这些已经训练好的模型,我们可以快速实现我们想要的功能。

Untitled/Untitled%208.png?imageView&thumbnail=400x400

face-api.js 的使用方法

引入方式

如果你不使用打包工具的话,可以直接导入 face-api.js 的脚本:dist/face-api.js 获得最新的版本,或者从 dist/face-api.min.js 获得缩减版,并且导入脚本:

<script data-original="face-api.js"></script>

如果你使用 npm 包管理工具,可以输入如下指令:

npm i face-api.js

初始化

我们之前说过,face-api 它实现了一系列的卷积神经网络,并针对网络和移动设备进行了优化。所有的神经网络实例在 faceapi.nets中获取到

var nets = {
    ssdMobilenetv1: new SsdMobilenetv1(), // ssdMobilenetv1 目标检测
    tinyFaceDetector: new TinyFaceDetector(),  // 人脸识别(精简版)
    tinyYolov2: new TinyYolov2(),   // Yolov2 目标检测(精简版)
    mtcnn: new Mtcnn(),   // MTCNN
    faceLandmark68Net: new FaceLandmark68Net(),  // 面部 68 点特征识别
    faceLandmark68TinyNet: new FaceLandmark68TinyNet(), // 面部 68 点特征识别(精简版)
    faceRecognitionNet: new FaceRecognitionNet(),  // 面部识别
    faceExpressionNet: new FaceExpressionNet(),  //  表情识别
    ageGenderNet: new AgeGenderNet()  // 年龄识别
};

其中 MobileNetsyolov2 是业内比较有名的目标检测算法,有兴趣的可以点击链接去看论文(我是看不懂),这篇文章 简要介绍了这些算法,大概就是说他们的检测速度和检测效率都不错。这里你可以根据自己的需要选择不同的算法,加载不同的模型。

官方推荐使用ssdMobilenetv1,因为它的识别精度比较高,但是检索速度相对较慢,如果是实时检测的场景,它的检索速度可能会成为问题,因此,今年下半年作者把 MTCNN 算法也引入了,如果想用实时人脸检测的场景,可以试试 MTCNN。(可以看看作者 这篇文章

模型加载

通过之前的介绍我们也可以知道,模型才是重中之重,有了训练好的模型,我们就可以跳过训练的阶段,直接使用来做人脸识别了。

这也就是国外一个机器学习的布道者 Dan Shiffman 在 视频 中一直所强调的:并不是所有的机器学习入门都应该从学习算法入手,毕竟术业有专攻,目前已经有很多人建立了很多成熟的模型(图形检测,文本识别,图像分类……),我们可以站在巨人的肩膀上去做更多有意思的事情。

face-api 本身也提供了一系列的模型数据(/weights),可以开箱即用:

await faceapi.nets.ssdMobilenetv1.load('/weights')

其中 /weights 是放了 manifest.json 和 shard 文件的目录,建议把官方的 weights 目录直接拷贝下来,因为经常你需要几个模型同时使用。

识别

face-api 提供了很多高级的方法可以使用,其中最方便的就是detectAllFaces / detectSingleFace(input, options) , 注意:它默认是使用SSD Mobilenet V1 ,如果要使用Tiny FaceDetector,需要手动指定:

const detections1 = await faceapi.detectAllFaces(input, new faceapi.SsdMobilenetv1Options())
const detections2 = await faceapi.detectAllFaces(input, new faceapi.TinyFaceDetectorOptions())

其中 detect 系方法都支持链式调用,因此你可以这样用:

await faceapi.detectAllFaces(input)
await faceapi.detectAllFaces(input).withFaceExpressions()
await faceapi.detectAllFaces(input).withFaceLandmarks()
await faceapi.detectAllFaces(input).withFaceLandmarks().withFaceExpressions()

获取识别数据

进行识别操作后,返回的数据是什么样的呢?

如果你是进行的全脸识别,那么数据是一个数组,其中 detection 是默认会有的属性,它提供了一个人脸部的盒子信息

[{detection:{
    box: {
        x: 156.22306283064717
        y: 76.60605907440186
        width: 163.41096172182577
        height: 182.21931457519534
        left: 156.22306283064717
        top: 76.60605907440186
        right: 319.63402455247297
        bottom: 258.82537364959717
        area: 29776.633439024576
        topLeft: Point
        topRight: Point
        bottomLeft: Point
        bottomRight: Point
    }
    ……
}]

如果你进行了链式操作,比如 withFaceLandmarks() 那这个对象会增加一个landmarks的属性,以此类推。

[{detection, landmarks, ……}]

其中landmarks提供了获取脸部各种特征点的方法:

const landmarkPositions = landmarks.positions  // 获取全部 68 个点
const jawOutline = landmarks.getJawOutline()  // 下巴轮廓
const nose = landmarks.getNose()  // 鼻子
const mouth = landmarks.getMouth()  // 嘴巴
const leftEye = landmarks.getLeftEye()  // 左眼
const rightEye = landmarks.getRightEye()  // 右眼
const leftEyeBbrow = landmarks.getLeftEyeBrow()  // 左眉毛
const rightEyeBrow = landmarks.getRightEyeBrow()  // 右眉毛

处理识别数据

要知道,你拿到的数据是根据图片的真实数据来处理的,但我们在网页展示的图片通常不会是 1:1 的实际图片,也就是说图片会进行缩放/扩大处理。比如一张图片是 1000x1000 的,图片上的人脸嘴巴可能在(600,500)这个位置,但是我们实际展示的是 600x600 的图片,如果根据(600,500)这个坐标去画,那早就在画布外了。

因此如果我想要在图片上做一点事情,我们需要把当前的数据进行一个转换,让它的数据匹配特定的大小,这里,可以用它提供的 matchDimensions(canvas, displaySize)resizeResults(result, displaySize) 方法:

// 把 canvas 固定到 displaySize 的大小
faceapi.matchDimensions(canvas, displaySize) 
// 把数据根据 displaySize 做转换 
const resizedResults = faceapi.resizeResults(detectionsWithLandmarks, displaySize)

其中 displaySize 是一个拥有{ width, height }的对象,所以你也可以直接传入带 width 和 height 的 DOM 元素,如 <canvas />, <img />

根据数据绘制图形

光拿到数据可没用,我们主要目的是为了绘制图形,在绘制这一块 face-api 也是提供了一系列高级方法,比如:

faceapi.draw.drawDetections(canvas, resizedDetections)  // 直接在识别区域画个框
faceapi.draw.drawFaceLandmarks(canvas, resizedResults)  // 直接画出识别的的特征点

Untitled/Untitled%209.png?imageView&thumbnail=400x400

(以下测试图片均是采用从百度搜「女生头像」搜到的小姐姐,如有侵权,请告知)

当然你还可以在特定位置画个框或文字,具体用法可以参考:DrawBoxDrawTextField

const drawOptions = {
  label: 'Hello I am a box!',
  lineWidth: 2
}
const drawBox = new faceapi.draw.DrawBox(resizedResults[0].detection.box, drawOptions)
drawBox.draw(canvas)

Untitled/Untitled%2010.png?imageView&thumbnail=400x400

圣诞帽的绘制

说了这么多,突然发现还没到我们的主题,画圣诞帽!让我们赶紧回来。

确定圣诞帽的位置

现在假定我现在拥有了所有的面部数据,我应该如何确定圣诞帽的正确位置?首先,我们必须明确一点,圣诞帽应该是要戴在头顶的,应该没有人把圣诞帽戴在眉毛上吧?(好吧当我没说)

Untitled/Untitled%2011.png?imageView&thumbnail=400x400

但是人脸识别的数据中一般是不包含头顶的,这可咋办?还好我小学一年级学过半个学期的素描,在素描中有个很重要的概念叫三庭五眼

Untitled/Untitled%2012.png?imageView&thumbnail=400x400

也是说正常人的发际线到眉骨的距离是眉骨到下颌距离的一半(作为程序猿的我表示,该规则可能已经不适用了)。

因此我们可以通过获取眉毛的坐标和下颌的坐标来计算出头顶的位置:

/**
 * 获取头顶的坐标
 * @param {*} midPos 眉心点坐标
 * @param {*} jawPos 下巴底点坐标
 */
const getHeadPos = (midPos, jawPos) => {
  // 获取线的 k 值
  const k = getK(midPos, jawPos);
  // 获取眉心到下颌的距离
  const distanceOfEye2Jaw = getDistance(midPos, jawPos);
  return getPos(k, distanceOfEye2Jaw / 2, midPos);
};

 在这里让我们复习几个解析几何的公式:

  • 两点之间距离公式:

Untitled/Untitled%2013.png?imageView&thumbnail=400x400

  • 根据两点确定斜率:

    Untitled/Untitled%2014.png?imageView&thumbnail=400x400

  • 点到直线的距离公式:

Untitled/Untitled%2015.png?imageView&thumbnail=400x400

  • 相互垂直的直线,斜率之积为-1

Untitled/Untitled%2016.png?imageView&thumbnail=400x400

要特别注意的是,由于 Canvas 默认的坐标系的结构和我们之前数学课上学的不太一样,我们把它逆时针旋转 90 度可以发现它的 x,y 轴跟我们认识的是反的,因此为了方便,我们通常在代入公式计算的时候把 x,y 进行一下调换。

Untitled/Untitled%2017.png?imageView&thumbnail=400x400

/**
 * 获取 K 值
 * @param {*} a
 * @param {*} b
 */
const getK = (a, b) => (a.x - b.x) / (a.y - b.y)

/**
 * 获取两点之间距离
 * @param {*} a
 * @param {*} b
 */
const getDistance = (a, b) => Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2));

因此目前已知眉心的坐标,下颌坐标,可以计算出这条直线的斜率,然后根据眉心到头顶的距离(眉心到下巴的一半),可以算出头顶的坐标:

/**
 * 已知 K,d, 点,求另一个点
 * @param {*} k 值
 * @param {*} d 距离
 * @param {*} point 一个基础点
 */
const getPos = (k, d, point) => {
  // 取 y 变小的那一边
  let y = -Math.sqrt((d * d) / (1 + k * k)) + point.y;
  let x = k * (y - point.y) + point.x;
  return { x, y };
};

图片合成

当我们已经知道了圣诞帽子的位置了,那接下的问题就是如何把圣诞帽合成到头像上去了,这里我们采用 Canvas 来实现 ,原理很简单:首先把头像绘制到 Canvas 上,然后再继续绘制圣诞帽就行了。

由于图片中可能不止一个面部数据,可能需要绘制多个帽子:

/**
 * 获取图片
 * @param {*} src 图片地址
 * @param {*} callback
 */
function getImg(src, callback) {
  const img = new Image();
  img.setAttribute('crossOrigin', 'anonymous');
  img.src = src;
  img.onload = () => callback(img);
}
/**
 * 绘制主流程
 * @param {*} canvas 
 * @param {*} options 
 */
function drawing(canvas, options) {
    const { info, width = 200, height = 200, imgSrc = 'images/default.jpg' } = options;
    const ctx = canvas.getContext('2d');
    // 重置
    ctx.clearRect(0, 0, width, height);
    // 先把图片绘制上去
    getImg(imgSrc, img => ctx.drawImage(img, 0, 0, width, height));
    // 循环把帽子画到对应的点上(由于图片中可能不止一个面部数据,可能需要绘制多个帽子)
    for(let i = 0, len=info.length;i < len;i++) {
        const { headPos } = info[i];
        getImg('images/hat.png', img => ctx.drawImage(img, headPos.x, headPos.y, 200, 120));
    }
}

我们可以看到帽子已经绘制上去了,但是位置有点奇怪:

Untitled/Untitled%2018.png?imageView&thumbnail=400x400

这是因为绘制图片的时候是按图片的左上角为原点绘制的,因此我们在实际绘制帽子的时候需要对坐标进行一个偏移:

/**
 * 根据我当前的圣诞帽元素进行一些偏移(我的图片大小是 200*130), 圣诞帽可佩戴部分的中心 (60,60)
 * @param {*} x
 * @param {*} y
 */
const translateHat = (x, y) => {
    return {
        x: x - 60,
        y: y - 60,
    };
};

function drawing(canvas, options) {
    ……
    const {x, y} = translateHat(headPos.x, headPos.y);
    getImg('images/hat.png', img => ctx.drawImage(img, x, y, 200, 120));
    ……
}

Untitled/Untitled%2019.png?imageView&thumbnail=400x400

这么看起来好多了,感觉自己棒棒哒!

Untitled/Untitled%2020.png?imageView&thumbnail=400x400

优化

但是有小伙伴可以会发现,这样的结果还是有点问题:

  • 帽子大小是固定的,但是头像的上的面孔可大可小,大脸放小帽子显然有点不合适。
  • 帽子的朝向是固定的,如果有人的头像是偏着的呢,帽子是不是也应该偏过来?

因此我们还需要继续做优化:

圣诞帽的大小

圣诞帽的大小我们可以根据识别出的脸的大小来确定,通常来说,帽子可戴的宽度等于脸宽就行了(考虑到展示效果可以略大),我这里强调可戴宽度是因为一个圣诞帽的图片中只有一部分是可戴区域

Untitled/Untitled%2021.png?imageView&thumbnail=400x400

// 0.7 为可戴区域占总区域的比重(为了让帽子更大一点,选择 0.6),0.65 是图片的宽高比
const picSize = { width: faceWidth / 0.6, height: (faceWidth * 0.65) / 0.6 };

而面部的大小可以通过 jawOutlinePoints的起始点距离来计算

/**
 * 获取脸的宽度(即帽子宽度)
 * @param {*} outlinePoints
 */
const getFaceWith = outlinePoints => getDistance(outlinePoints[0], outlinePoints[outlinePoints.length - 1])

圣诞帽的角度

圣诞帽的角度该如何计算呢?其实也很简单,知道头的偏转角度就行了,而头的偏转角度,直接用脸中线(眉心到下颌)和 y 轴的夹角就行了(直接用 atan2 算出来的是补角,需要用 180 度减),这里考虑到后续使用方便直接用的弧度。

/**
 * 获取脸的倾斜弧度
 * @param {*} jawPos
 * @param {*} midPointOfEyebrows
 */
const getFaceRadian = (jawPos, midPointOfEyebrows) =>
    Math.PI - Math.atan2(jawPos.x - midPointOfEyebrows.x, jawPos.y - midPointOfEyebrows.y); //弧度  0.9272952180016122

Untitled/Untitled%2022.png?imageView&thumbnail=400x400

Canvas 中图片旋转

注意,在 Canvas 中没办法直接旋转图片,只能旋转画布,而且画布是按照原点旋转的,这点会特别坑。

Untitled/Untitled%2023.png?imageView&thumbnail=400x400

这里,我们只想让图片按照中心旋转怎么办?我们可以先让 Canvas 把原点平移到帽子的位置,然后再通过帽子的内部偏移使得帽子中心刚好在原点,此时再旋转画布把帽子画上就只影响这一个帽子图片了。

Untitled/Untitled%2024.png?imageView&thumbnail=400x400

/**
 * 绘制帽子 
 * @param {*} ctx 画布实例
 * @param {{}} config 配置
 */
function drawHat(ctx, config) {
    const { headPos, angle, faceWidth } = config;
    getImg('images/hat.png?imageView&thumbnail=400x400', img => {
        // 保存画布
        ctx.save();
        // 画布原点移动到画帽子的地方
        ctx.translate(headPos.x, headPos.y);
        // 旋转画布到特定角度
        ctx.rotate(angle);
        // 偏移图片,使帽子中心刚好在原点
        const { x, y, width, height } = translateHat(faceWidth, 0, 0);
        // 我的圣诞帽子实际佩戴部分长度只有 0.75 倍整个图片长度
        ctx.drawImage(img, x, y, width, height);
        // 还原画布
        ctx.restore();
    });
}

这样整个绘制的主流程大概就是这样:

function drawing(canvas, options) {
    const { info, width = 200, height = 200, imgSrc = 'images/default.jpg'} = options;
    const ctx = canvas.getContext('2d');
    // 重置
    ctx.clearRect(0, 0, width, height);
    // 先把图片绘制上去
    getImg(imgSrc, img => ctx.drawImage(img, 0, 0, width, height));
    // 循环把帽子画到对应的点上
    for (let i = 0, len = info.length; i < len; i++) {
        drawHat(ctx, info[i]);  
    }
}

成品展示

Untitled/Untitled%2025.png?imageView&thumbnail=400x400

Untitled/Untitled%2026.png?imageView&thumbnail=400x400

Untitled/Untitled%2027.png?imageView&thumbnail=400x400

Untitled/Untitled%2028.png?imageView&thumbnail=400x400

image-20191223201537510

是不是感觉已经很完美了?目前已经能实现对各种大小的脸,不同朝向的脸进行适配了。甚至连狗狗的头像也能识别出来,这都得利于 face-api.js 提供的模型中也有狗狗的脸训练数据。

当然,这个例子实际上还是很不完善,因为当你测试的图片多的时候就会发现,对侧脸头像的识别还是有问题:

image-20191223201619850

这是因为脸的大小用之前的方法计算的其实是不准确的,实际上还应该根据双边眼睛的比例知道用户现在是侧脸还是正面,从而继续调整帽子的大小和位置。但是这里就不继续优化了,感兴趣的小伙伴可以自己去琢磨琢磨如何修改。

写在最后

通过上面这个小例子,我们可以发现前端利用机器学习也可以实现很多不可思议的有趣玩意,再结合 VRARwebRTC,我们甚至可以做一个纯前端的互动小游戏。

当然就算在这个例子上,你也可以进行扩展,比如绘制圣诞胡须,化妆……

如果你想继续深入学习机器学习的相关知识,可以去看看 Coursea 的 机器学习入门课程,如果你想深入学习一下 Tensorflow,可以去看看 Tensorflow.js 的 官方文档

虽然之前有吐槽 Tensorflow.js 知识点太多的问题,但是不得不说 Google 的文档写的还是不错的,提供了很多案例,手把手教你如何实现一些简单的功能:手写数字识别,预测,图片分类器……所以对 Tensorflow.js 感兴趣的童鞋不妨去它的官方文档中逛逛。

不过毕竟 Tensorflow.js 还是很底层的库,如果你只是想用机器学习做一些有趣的事情,不妨尝试一下 ml5.js,这里有一套学习视频

最后,祝大家圣诞快乐!

参考

https://github.com/justadudewhohacks/face-api.js

face-api.js — JavaScript API for Face Recognition in the Browser with tensorflow.js

https://tensorflow.google.cn/js/tutorials

https://www.youtube.com/watch?v=jmznx0Q1fP0

https://www.cnblogs.com/suyuanli/p/8279244.html

https://learn.ml5js.org/docs/#/

本文发布自 网易云音乐前端团队,文章未经授权禁止任何形式的转载。我们一直在招人,如果你恰好准备换工作,又恰好喜欢云音乐,那就 加入我们
查看原文

赞 65 收藏 35 评论 1

Tenadolanter 提出了问题 · 2019-12-25

vue中的context和store区别在哪里

根据文档,vuex中的context拥有store的所有属性和方法,那么他们的区别在哪里呢?

new vuex.store({
    state: {},
    getters: {},
    mutations:{},
    actions:{
        increase(context){
            // 拥有state、getters、commit、dispatch
            // replaceState、registerModule、unregisterModule
            context.commit()
        }
    },
    modules:{}
})

关注 4 回答 2