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

认证与成就

  • 获得 21 次点赞
  • 获得 10 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 10 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-10-15
个人主页被 515 人浏览