Git Product home page Git Product logo

set.sh-stale's Introduction

💻📑在线浏览

Changelog

CHANGELOG

Extracted dependencies

  • docs-server 基于 Koa2 的一种文档类型 server 自动构建实现。

  • grid-style 基于 flexbox 实现的 CSS Grid layout,其中包含响应式元素。

  • mark-to-jsonmarkdown 文件转换为 JSON 静态文件的实现。

Writing

博客中记录的是学习心得和思考总结,主要偏向宏观思考方面。

Skills

Issues 中记录了开发过程中遇到的部分问题的解决方案,主要偏向实际应用微观层面。

License

知识共享许可协议
本作品由 Bowen 创作,采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。

set.sh-stale's People

Contributors

lbwa avatar

Watchers

 avatar

set.sh-stale's Issues

fiddler 前端请求拦截(代理)

下文假定读者已经在电脑上正确安装了 fiddler

配置正确的 fiddler 设置

  • 配置拦截指定设备的 HTTPS 请求

Tools --> Options --> HTTPS

在该选项卡内配置 Capture HTTPS CONNECTs 和该选项下的 Decrypt HTTPS traffic

  • 配置局域网设备拦截

Tools --> Connections --> Allow remote client to computers connect

配置局域网设备

  • 局域网设备证书配置(IOS 为例)

    1. 配置解密证书,局域网设备使用 safari 访问 <fiddler 所在设备的局域网 IP>:<fiddler 配置的端口,默认 8888>(如:192.168.0.108:8888),安装该网页下的 FiddlerRoot certificate 证书

    2. 信任证书,在 设置 --> 通用 --> 描述文件 中信任下载的证书。

    3. 启用证书,在 设置 --> 通用 --> 关于本机 --> 证书信任设置 中对之前下载的证书进行信任操作。

  • 局域网设备网络配置

  1. 局域网设备与 fiddler 设备保持在同一局域网中。

  2. 在局域网设备连接 wifi 时,手动配置代理为 <fiddler 所在设备的局域网 IP>:<fiddler 配置的端口,默认 8888>

以上配置之后,局域网配置的所有请求都将被 fiddler 代理(转发)。其中因为局域网设备安装了 fiddler 的解密证书,那么所有被监听设备(包含 fiddler 本机)的 HTTPS 请求将被解密。

函数式编程 —— Promise 序列实例化

实现 Promise 的顺序实例化,即仅当前一个 Promise 实例化状态 resolved 之后才开始调用下一个 Promise 实例化。此举不同于 Promise.all[].map() 中的并发实例化 Promise

/**
 * 
 * @param {promise[]} funcs 待实例化数组
 */
const promiseSerial = funcs => 
  funcs.reduce((promise, func) =>
    // 待上一次 promise 的状态 resolved 之后,调用下一个 promise,即 func
    // 并将运行 func 得到的结果与之前的 promise 返回的结果合并
    promise
      .then(result => func().then(Array.prototype.concat.bind(result)))
      .catch(console.error),
    // 初始值为一个空数组容器
    Promise.resolve([])
  )

Reference

mini-css-extract-plugin 报错冲突

报错信息如下:

4C2DABCC-DC5E-4143-8685-0767C39E9192

官方解答在这里

We are trying to generate a CSS file but your codebase has multiple possible orderings for these modules.

官方给出的两种解决方式为:

There are two things that you can do:

Sort your imports to create a consistent order. I must admit that the warning could be improved to point to the locations that need to be changed.

Ignore the warning with stats.warningFilter when the order of these styles doesn't matter. Maybe the eventually add an option to mark order-independent styles directly.

async 函数中的并发

async 函数中的并行执行

async 函数中,仅当存在 await 关键词的地方表示需要等待计算结果。那么存在互不依赖的操作时,不要错失并行执行的机会。

Notice: 以下示例代码中的 wait() 方法表示异步操作。因为在 async 函数中的同步操作与有没有 await 不对整个 async 函数内部执行顺序造成影响。因为函数体中的同步操作将按照常规单线程 event loop 执行,await 后如果是非 Promise 对象,那么 await直接 返回一个包含该同步操作的 Promise 对象。

async function series () {
  // 需要等待 wait(500) 的计算结果才能继续执行函数体
  await wait(500)
  await wait(500)
}

以上代码中,op0op1 的运行关系是同步执行,即只有等到 op0 执行完成后才会执行 op1

async function parallel () {
  // 没有 await 关键词,那么将直接调用 wait(500),执行该异步操作。
  const op0 = wait(500)

  // 因为前一个 op0 是异步操作,且没有 `await`,根据 `ES` 规范中 `event loop` 的描述,
  // `new Promise(...)` 参数函数在本轮 `event loop` 中立即执行,之后该 `promise` 实例将脱离
  // execution context stack 在堆内存中等待异步执行结果。
  // 所以不论 op0 的异步操作有没有执行完成,此时将继续调用一个新的异步操作 wait(500)
  const op1 = wait(500)

  // 此时 op0 与 op1 是并行执行,并且此时在同时等待二者的执行结果
  // 那么整个 parallel 执行时间将缩短
  const result0 = await op0
  const result1 = await op1
}

以上代码 op0op1 将并行执行。

同时还存在以下一种并行执行的写法:

async functon parallel () {
  const [result0, result1] = await Promise.all([wait(500), wait(500)])
}

Reference

vue style collections

vue 中的 CSS 作用域

通过定义独有的 hash 属性的样式选择器来实现。

在使用 vue-cli 的 webpack 模板开发 SPA 时,遇到导入的样式一部分生效,一部分不生效,且在 chrome 开发工具中,某些元素带有形如data-v-10578a49的属性。

经查阅官方文档vue-loader,和组件类注释,知道在单文件组件中有一个极易忽视的属性scoped,它的出现表示该单文件组件的样式,只限定于该单文件组件。就算样式中含有其他组件的样式,最后也会被忽略

只要单文件组件中的样式带有scoped属性,那么在编译时,将为样式指定一个形如data-v-10578a49的属性(就像id一样)来指定其组件样式的单独作用域

Object.defineProperty 和 Proxy 实现数据代理

MVVM 框架的个人见解

👉 MVVM架构模式

响应式原理以及数据双向绑定

👉 Repo: vue-reactive

  • Model 经由 ViewModelVue.js) 绑定 View;监听 View 变化以通过 ViewModel 更新 Model

数据代理实现

  • this.$data[key] === this[key] 成立。

Vue.js 现阶段使用 Object.definePropertyReflect.defineProperty 将逐步取代之) 实现数据代理。

// src/core/instance/state.js
const baseDescription = {
  enumerable: true,
  configurable: true,
  get: function () {},
  set: function () {}
}

function defineProxy (target, sourceKey, key) {
  baseDescription.get = function proxyGetter () {
    return this[sourceKey][key]
  }

  baseDescription.set = function proxySetter (value) {
    this[sourceKey][key] = value
  }
  Reflect.defineProperty(target, key, baseDescription)
}

// 简单实现数据代理
const vm = {
  data: {
    name: 'John Wick',
    num: 20
  }
}

const keys = Object.keys(vm.data)
const len = keys.length
for (let i = 0; i < len; i++) {
  defineProxy(vm, 'data', key[i]) // vm: { data: {...}, name: 'John Wick', num: 20 }
}

同时,ES2015 中新增的 Proxy 方法同样可以实现数据代理,在 Vue.js 3 中 Proxy 可能代替 Object.defineProperty

// 示例 1
const target = {
  keys: {
    name: 'I am a original object.'
  }
}

const handle = {
  get (target, key) {
    return key in target.keys? target.keys[key] : console.error(`There is no '${key}' in `, target.keys)
  }
}

// 通过 Proxy 实例来读取被代理的对象的键值
const proxy = new Proxy(target, handle)
proxy.name // 'I am a original object.'

// 示例 2
const target = {
  keys: {
    name: 'I am a original object'
  },

  // getter 函数必须在 target 的对象内设置,而非在 receiver 设置
  get name() {
    return this.keys.name
  }
}

/**
 * 1. receiver 仅起到提供 键值 的作用,不在其中设置 getter 或 setter。当遇到
 * target 内有 getter 函数时,将 receiver 提供给 target 中的 getter 函数
 */
const receiver = {
  keys: {
    name: 'I am a super object'
  }
}

Reflect.get(target, 'name', receiver) // "I am a super object"


// 示例 3
const target = {
  keys: {
    name: 'I am a original object'
  },

  // getter 函数必须在 target 的对象内设置,而非在 receiver 设置
  get name() {
    return this.name
  }
}

// receiver 仅起到提供 键值 的作用,不在其中设置 getter 或 setter
const receiver = {
  name: 'I am a super object'
}

const handle = {
  // 此处若传入第三个参数,那么第三个参数将表示当前的 proxy 实例
  get (target, key) {
    return Reflect.get(target, key, receiver) // 相当于 target.keys[key] 作用
  }
}

const proxy = new Proxy(target, handle)
proxy.name // 'I am a super object'

Engineering collections

规范 commit log

推荐工具:commitizen

  • 安装

(以项目级安装为例)

yarn run commitizen cz-conventional-changelog -D
  • 配置

(in package.json

"script": {
  "commit": "yarn run lint && git add . && git-cz"
},
"config": {
  "commitizen": {
    "path": "./node_modules/.bin/cz-conventional-changelog"
  }
}

Notice:推荐在提交前执行 lint 检查并自动修复,eslint 需另外配置。

"script": {
  "lint": "./node_modules/.bin/eslint --fix --ext .js lib/"
}

更新版本

version x.y.z

# 修复 bug 等小修改,并向下兼容
npm version patch <=> z++

# 增加新功能,并向下兼容
npm version minor <=> y++ && z=0

# 重构等破坏性升级,不兼容之前的版本
npm version major <=> x+= && y=0 && z=0

或者使用 yarn 升级版本:

# yarn 1.7.0+ 版本
yarn version --patch
yarn version --minor
yarn version --major

yarn 的特别之处在于执行命令行指令的同时会默认执行 git tag v1.0.0 打上无注释标签npm version 默认关闭此功能。

开源项目更新版本流程

  1. 更新开源库代码之后,执行 eslint 检查。

  2. npm version [patch|minor|major] 更新版本号。

    • 不使用 yarn 是为了分离更新版本号和 git tag v1.0.0 命令,使得生成 tag 时包含最新的代码更新。或者执行 yarn config set version-git-tag false 来关闭默认的生成标签行为(more detail)。

    • 若不关注最新构建后的代码是否存在版本号,那么可直接使用 yarn 的相关命令。但必须在更新版本号前 commit 最新代码至仓库,一定要保证生成 tagcommit 之后。

  3. 打包构建最新版本源码,其中 包含最新版本号

  4. 使用 commitizen 规范地提交本地代码更新至本地仓库。

    • commitizen 的执行必须先于生成 tag
  5. 将本地仓库与远程仓库同步。

    • 此时确保 CI 能够 正常部署,否则修改源码回到第 1 步。
  6. git tag v[version number] 生成标签,此时标签包含了最新的代码更新

    • 在最后生成 tag 的目的就是要让最新的标签包含最新的代码更新(其中有包含最新版本号的构建后代码)。若在之前使用 yarn 更新版本号,那么新的标签就不会包含最新的代码更新(因为没有 committag 是打在 commit log 上的),并且之后脚本将生成错误的 CHANGELOG
  7. 将本地 tags 与远程 tags 同步。

    • git push origin --tags
  8. 更新 CHANGELOG

    • 因为脚本生成 CHANGELOG 是根据 tag 记录和 commit log 生成。
  9. 使用 commitizen 规范提交最新的 CHANGELOG 至本地仓库。并同步至远程仓库。

  10. yarn publish --new-version [version number] 发布至 npm

以上所有步骤可编写为一个脚本实现(extension)。

Reference

Recommended workflow

类库打包格式

推荐工具:rollup

  • amd – 异步模块定义,用于像 RequireJS 这样的模块加载器
define(`${moduleName}`, ["vue"], factory)
  • cjsCommonJS,适用于 NodeBrowserify/Webpack
// CommonJS 2
module.exports = factory(require("vue"))

// CommonJS
exports[`${moduleName}`] = factory(require("vue"))
  • es– 将软件包保存为 ES 模块文件
export default moduleName
  • iife – 一个自动执行的功能,适合作为 <script> 标签(source)。

  • umd – 通用模块定义,兼容 amdcjsiife

Sass 学习心得

预编译目标格式.photo-#{list}

.author-bio .photo-adam {
  background: url("./img.png") no-repeat;
}
.author-bio .photo-john {
  background: url("./img.png") no-repeat;
}
.author-bio .photo-wynn {
  background: url("./img.png") no-repeat;
}
.author-bio .photo-mason {
  background: url("./img.png") no-repeat;
}
.author-bio .photo-jim {
  background: url("./img.png") no-repeat;
}

即形如.photo-#{list}的样子,一部分属性键名是个固定的,另一部分键名是某 list 中的某一个字符串。
Scss 文件为:

$list: adam john wynn mason jim // 列表
@mixin author-images {
  @each $author in $list {      // 循环列表每一项
    .photo-#{$author} {         // Scss 插值,#{ 插值内容 }
      background: url('./img.png') no-repeat
    }
  }
}

.author-bio {
  @include author-images;
}

以上方法的一个局限性是,要有另外一个类.author-bio来使用@include调用混合宏@mixin

@forwhile循环相比,@for $i from <start> through <end> 是在一定的数值范围内进行循环;while是当@while 表达式中表达式为时,就会执行代码块;@each 变量 in $list循环的是一个自定义list列表。

使用 map 管理变量

格式如下:

$social-colors: (
    dribble: #ea4c89,    // 以逗号分隔
    facebook: #3b5998,
    github: #171515,
    google: #db4437,
    twitter: #55acee     // 最后一对键值对没有逗号结尾
);

其中,格式类似于JSON的格式。该数据结构通过键名查找键值。该map结构的优势在于可集中管理变量,并后期可以很容易的添加或或删除变量数据。

map-get($map, $key) 获取数据

使用map-get($map, $key)来获取$map$key的数据。若$map中不存在$key的数据。那么将不会编译该条 CSS,此时,并不会报错。为了防止这种情况,在调用map-get($map, $key)之前先使用map-has-key($map,$key)来检测对应值是否存在。

map-has-key($map,$key) 检测是否存在数据

map-has-key($map,$key) 检测是否存在$key数据,该函数返回一个布尔值,可在使用map-get($map, $key)之前添加一个检测判断以防止因没有对应$key数据,而不编译 CSS 的情况。

格式如下:

@if map-has-key($social-colors,facebook){  // 判断值是否否存在
    .btn-facebook {
        color: map-get($social-colors,facebook);
    }
} @else {
    @warn "No color found for facebook in $social-colors map."
}

为了简化检测值的代码,避免过多的 if 语句,可将判断封装在自定义函数中,代码如下:

@function colors($color) {
  @if not map-has-key($social-colors, $color){
    @warn "No color found for `$color` in $social-colors."
  }
  @return map-get($social-colors, $color)  // 返回调用的 map 函数,类似惰性加载机制
}

注:可能是在命令行的开头位置看见警告信息!

例如:

$ sass test1.scss --style expanded
WARNING: No color found for baby-color in $social-colors.
         on line 23 of test.scss, in `colors`
         from line 29 of test.scss

.btn-github {
  color: #171515;
}

map-keys($map) 结合 $list 使用

map-keys($map)返回$map中的所有 key,这些值赋予给一个变量。

示例为:

$list: map-keys($social-colors);

之前的自定义函数可修改为:

@function colors ($color) {
  $name: map-keys($social-colors);
  @if not index($name, $color) {  // 此处 index() 可替换为 @for 或 @each 
    @warn "#{$color} is not a valid color name.";
  }
  @return map-get($social-colors, $color);
}

与之前使用map-has-key($map, $key)的区别在于,使用map-keys($map)返回一个列表(取出所有 key ),是在该列表中查询是否存在对应 key ,而使用map-has-key($map, $key)是在map中查找对应 key 。

map-values($map)的用法与map-keys($map)相似,只是map-values($map)是取出所有的 value 值。

map-merge($map1, $map2) 合并两个 map

格式如下:

@new-map: map-merge($color, $color1);

其中,当map1map2存在相同键名的值对,那么map2将覆盖map1的数据。

map-remove($map, $key) 删除某一 $key 得到新的 map

$map:map-remove($social-colors, key-of-remove-item);

注意,该方法返回的是一个新的 map 数据结构,对原结构不构成影响。

@import

Sass 扩展了 CSS 的 @import 规则,让它能够引入 SCSS 和 Sass 文件。 所有引入的 SCSS 和 Sass 文件都会被合并并输出一个单一的 CSS 文件。 另外,被导入的文件中所定义的变量或 mixin 都可以在主文件中使用。

@media

Sass 中的 @media 指令和 CSS 的使用规则一样的简单,但它有另外一个功能,可以嵌套在 CSS 规则中。有点类似 JS 的冒泡功能一样,如果在样式中使用 @media 指令,它将冒泡到外面。

HTML & CSS collections

CSS values

  • 滚动条出现时,防止页面跳动
html
  width: 100vw
  overflow-x: hidden

100vw 表示撑满整个视口,overflow-x: hidden 表示隐藏横向的溢出,那么此时即使横向内容溢出,横向滚动条也不会出现。

vw 本质是忽略了滚动条宽度。(w3c

when the value of overflow on the root element is auto, any scroll bars are assumed not to exist. Note that the initial containing block’s size is affected by the presence of scrollbars on the viewport.

那么在 vw 生效时,当 overflow-y (此时不是 overflow-x)为 auto 时将忽略 所有 上下滚动条的宽度。vh 同理。

对象类型、文本框值、函数和变量之间的提升优先级

1. if语句中的判断语句中的值的检测

一般情况下,基本类型值可用typeof操作符检测,但是对象类型的值则应该使用instanceof操作符来检测。

特别是对象类型的检测,因为当我们只针对某一特性检测时,如下:

if(typeof foo.sort == 'function'){
  foo.sort();
  foo.reverse();
}

以上语句首先检查foo.sort方法是否存在。这样,当我们传入一个同样包含名为sort方法的其他对象(而不是数组),此时,也能通过检测,但在调用reverse()方法时就会报错了。
以上若是修改为以下代码:

if(foo instanceof Array){
  foo.sort();
  foo.reverse();
}

这时instanceof操作符检测了foo是否是Array构造函数的原型对象的属性之一,确保了foo是Array类型的实例,这样就屏蔽了所有非数组的值了。

2. 得到对象准确类型的方法

当不知道对象的准确类型,要得到对象类型最准确的方法是使用Object.prototype.toString.call( 待测对象 )方法。

let a =[1,2,3,4];
Object.prototype.toString.call(a);  // "[object Array]"

调用该方法时,返回的结果是"[object ${ 对象的类型 }]"
该方法与instanceof相比,是用来得到对象的准确类型。而instanceof的应用场景是偏向于确定是否是某一类型的对象,重点在是否

3. 对input之类的输入文本框取值,得到的数据类型是字符串

因为输入框取值得到的数据类型是String,所以在后期如果要将他们字符串中的值相加的话,要先转换类型再相加,如'60'+'6' != '66'。由此,推广出,数值的调用自己心里要清楚调用的数据是什么类型,再做相应的相加操作。否则,可能会得到一些莫名其妙的结果。

Babel 的基本使用手册

@babel/preset-env

@babel/preset-env 一种允许开发人员使用最新版本的 JavaScript,而不用对代码格式及特性(另有浏览器 polyfill)进行微管理的智能预设工具集合。

@babel/preset-env 可在配置文件中指定 target.browsers 项或通过 browserslist 来指定需要转译的目标浏览器。

  • target.browsers 项可在 .babelrc 文件中配置,或在对应的 webpack loaderoptions 项下配置。

    {
      "presets": [
        "@babel/preset-env",
        {
          // 是否开启将 ES6 的 module 格式转换为其他 module 类型,此处排除转
          // 换是为了让 webpack 来处理 ES6 模块(如,实现模块按需加载)
          "modules": false,
          // 此配置项是处理 `@babel/preset-env` 如何引入 `@babel/polyfill`
          "useBuiltIns": "usage",
          // 目标浏览器,若 `package.json` 中存在 `browserslist` 项则优先生
          // 效 `package.json` 中的 `browserslist` 项
          "targets": {
            // browsersklist 可配置项有:
            // https://github.com/browserslist/browserslist#full-list
            "browsers": ["> 1%", "last 2 version"]
          }
        }
      ]
    }
  • browserslsit 有单文件和集成在 package.json两种方式

    // package.json (推荐方式)
    {
      "browserslist": ["> 1%", "last 2 version"]
    }

另外在 .babelrc 配置文件中,应特别注意配置项 useBuiltIns,该配置项用于配置 @babel/preset-env 该如何引入 @babel/polyfill

  • useBuiltIns: 'entry'

    该选项将开启一个新插件,该插件用于根据源代码文件中语句 import '@babel/polyfill'require('@babel/polyfill') 来导入特定的所需 polyfill

    • In

      import '@babel/polyfill'
    • Out

      // 只引入特定的 ployfill
      import 'core-js/modules/es7.string.pad-start'
      import 'core-js/modules/es7.string.pad-end'
    • useBuiltIns: 'usage'

      polyfill 在一些文件中被引入时,为 polyfill 新增一个导入(因为 Webpack正确配置后,会将多处相同引用,指向同一 bundle)。我们利用的是一个 bundle 只加载相同的 polyfill 一次这一原理。

      • In

        var a = new Promise()
      • Out(如果执行环境不支持该特性)

        import 'core-js/modules/es6.promise'
        var a = new Promise()
      • Out(如果浏览器支持该特性)

        var a = new Promise()
    • useBuiltIns: false
      不会在每个文件中自动加入 polyfill,或转换 import '@babel/polyfill 为一些单独的 polyfills

@babel/polyfill

Website

babel 包含一个定制化的 regenerator runtimecore-js

@babel/polyfill 用于模拟整个 ES2015+ 的执行环境(不包含 Stage 4 提案),并旨在在应用中被使用,而不是一个库或工具(该 polyfill 将在使用 babel-node 时自动引入)

这意味着你可以使用新的内置插件,像 PromiseWeakMap,静态方法像 Array.fromObject.assign,实例方法像 Array.prototype.includes,和 generator 函数(提供给你 regenerator 插件)。本 polyfill 为了实现以上目标,向 全局作用域 和像 String 这样的 原生原型 上添加了这些方法。

Polyfill 打包大小

这个 polyfill 给我们提供了便利,但是你应使用 @babel/preset-env@babel/preset-envuserBuiltIns 选项来实现使用 @babel/polyfill,以至于最终的打包结果不会包含你不需要的 polyfill,即实现 polyfill 的按需加载 。否则,我们建议你手动导入 单个 polyfills

结合 babel-loader 和 @babel/preset-env 按需导入 polyfill

注意 ,此时 不需要显式地 在应用代码中引入 polyfill,即不需要在入口文件 import '@babel/polyfill。此时仍需安装 @babel/polyfillbabel-loader@babel/preset-env 会根据配置自动实现按需加载 polyfill

// webpack.config.js
rules: [
  // transform-runtime 插件用于告诉 Babel 引入 runtime 而不是内联  runtime,即将
  // 所有的 helpers 函数分离为单独的 runtime 以实现代码复用。
  {
    test: /\.m?js$/,
    exclude: /(node_modules|bower_components)/,
    use: {
      loader: 'babel-loader',
      options: {
        // 此处将根据上文 @babel/preset-env 的示例代码配置,自动引入所需的 polyfill
        // 而不是将整个 polyfill 全部引入
        presets: ['@babel/preset-env'],
        plugins: ['@babel/plugin-transform-runtime']
      }
    }
  }
]

Note:
babel 未转译特定的代码为 ES5(即未引入特定的 polyfill,或 polyfill 未生效),应注意检查 babel-loader 是否有配置 options 项。否则 babel-loader 将不执行特定的代码转译。

babel-loader 的相关配置可见 Github repo

@babel/plugin-transform-runtime

Website

一个用于开启复用 babel 注入的 helper 帮助函数(即用于辅助 babel 转译代码的函数)的插件。即,处于多个文件中的相同的 helper 帮助函数,将被提取到 babel runtime 中以实现代码复用,减小打包体积。

推荐的配置方式是通过 .babelrc,其他方式可参考官方文档

{
  "plugins": [
    "@babel/plugin-transform-runtime"
  ]
}

JS 迭代器实现

function myIterator (arr) {
  let __index = 0
  return {
    // 迭代器对象核心
    // arr[__index++] 为示例目标值访问接口,可替换为其他类型值访问接口来实现其他类型迭代
    next () {
      return __index < arr.length ?
      	{value: arr[__index++], done: false} : 
        {value: undefined, done: true}
    }
  }
}

const it = myIterator([1, 2, 3, 4])

// output
const c = document.createElement.bind(document)
function createFragment (result) {
  const fragment = c('div')
  fragment.innerText = result
  return fragment
}

(function insert () {
  let i = 5
  const chip = c('div')
  while (i--) {
    chip.appendChild(createFragment(JSON.stringify(it.next())))
  }
  document.body.appendChild(chip)
})()

Play on the playground

给 vuex store 添加热更新功能

import Vuex from 'vuex'
import createLogger from 'vuex/dist/logger'

import defaultState from './state/state'
import getters from './getters/getters'
import mutations from './mutations/mutations'
import * as actions from './actions/actions'

const debug = process.env.NODE_ENV === 'development'

// ssr 时,保证每次渲染去掉对之前的 store 引用,对应一个新的 store,以防止内存溢出。
/* eslint-disable no-new */
export default () => {
  const store = new Vuex.Store({
    state: defaultState,
    getters,
    mutations,
    actions,

    // 限定只能通过  mutations 修改 state,不应用于生产环境
    strict: debug,

    // 使用提交 mutations 和 actions 在控制台输出的插件
    plugins: debug ? [createLogger()] : []
  })

  // 给 store 添加热模块加载功能
  if (module.hot) {
    module.hot.accept([
      './state/state',
      './getters/getters',
      './mutations/mutations',
      './actions/actions'
    ], () => {
      // import 语句不能写在业务代码块中,只能写在外部
      const newState = require('./state/state').default
      const newGetters = require('./getters/getters').default
      const newMutations = require('./mutations/mutations').default
      const newActions = require('./actions/actions').default

      store.hotUpdate({
        state: newState,
        getters: newGetters,
        mutations: newMutations,
        actions: newActions
      })
    })
  }
  return store
}

webpack-dev-server 代理跨域请求 / 模拟本地后端

我的一篇关于跨域解决方案的博文

1. 模拟本地后端

通常在前端开发的过程中,有使用 axios-mock-adapter 工具模拟后端返回的方法,本文旨在记录另一种通过配置 wepack-dev-server 来模拟后端返回数据的方法。

涉及背景知识

在 webpack 配置中 devServer.before 用于在服务器内部所有中间件执行前定义自定义处理程序,即此选项可在本地模拟服务器数据返回。

webpack-dev-serverreadme 部分有如下介绍:

Most new feature requests can be accomplished with Express middleware; please look into using the before and after hooks in the documentation.

由此,可知我们可在 webpack 中配置 devServer.before 且该方法是具有 Express 中间件的 API

配置方法

配置文件代码如下:

const appData = require('../mock/data.json') // 引入 mock 数据文件

const seller = appData.seller  // 定义不同 api 返回的数据
const goods = appData.goods
const ratings = appData.ratings

const devWebpackConfig = {
  // ...
  devServer: {
    before (app) {
      app.get('/api/seller', (req, res) => {   // 使用 app.get() 方法模拟后端
        res.json({  // 使用 res.json() 方法转换为 JSON 格式
          errno: 0,
          data: seller  // 指定地址匹配时,应返回的数据
        })
      })

      app.get('/api/goods', (req, res) => {
        res.json({
          errno: 0,
          data: goods
        })
      })

      app.get('/api/ratings', (req, res) => {
        res.json({
          errno: 0,
          data: ratings
        })
      })
    }
  }
  //...
}

示例配置

点我可见示例配置

2. 代理跨域请求

开发过程中,跨域请求真实后端接口时,因 jsonp 无法伪造 Request Header 中的 Host 和 referer,而造成 5XX 服务器错误

解决方案

可设置 devServer.before( ) ,之后让 app ajax 请求 webpack-dev-server 地址,让 webpack-dev-server 代理请求后端接口。

示例代码如下:

  devServer: {
    // app 请求 webpack-dev-server 地址
    // webpack-dev-server 代理发送 ajax 请求
    before (app) {
      app.get('/api/getPlayList', (req, res) => {
        var url = 'https://c.y.qq.com/splcloud/fcgi-bin/fcg_get_diss_by_tag.fcg'

        axios.get(url, {
          headers: {
            referer: 'https://c.y.qq.com',
            host: 'c.y.qq.com'
          },
          params: req.query
        }).then(response => {
          res.json(response.data)
        }, e => {
          throw Error('Proxy failed')
        })
      })
    },
    // ...
   }

示例配置

点我可见示例配置

设置 Vue.js 中的 .jsx 的样式作用域 —— CSS Modules

原理

CSS Modules 可起到与 #6 scoped 相同的作用,即定义样式的作用域。相关介绍点我查看

webpack 配置

// webpack.dev.config.js

// 生成独一无二的 class / id 值
const createLocalIdentName = process.env.NODE_ENV === 'development'
  ? '[path]-[name]-[hash:base64:5]'
  : '[hash:base64:5]'

module.exports = merge(baseWebpackConfig, {
  // ...
  module: {
    rules: [{
      test: /\.scss$/,
      include: [resolve('src')],
      use: [
        'style-loader', // 将样式表写入 HTML 中
        // 'css-loader',
        {
          loader: 'css-loader',
          options: {
            // CSS modules 模拟 scoped ,此处用于定义 .jsx 的样式作用域
            // https://vue-loader.vuejs.org/zh-cn/features/css-modules.html
            modules: true,

            // 实现样式独有的命名空间(作用域)
            localIdentName: createLocalIdentName
          }
        },
        // ...
      ]
    }]
  }
  // ...
}

易错点

在以上配置完成后,若项目中有字体文件,将可能发生路径解析错误。

如下:

@font-face {
  font-family: 'fontello';
  src: url('~@/common/font/fontello.eot?81914681');
  // ...
}

此时,webpack 无法正确解析别名路径。

解决方案是将路径写为基于根目录的文件路径,原因点我:

@font-face {
  font-family: 'fontello';
  src: url('/src/common/font/fontello.eot?81914681');
  // ...
}

jsx 配置

import className from 'scss/layout-footer.scss'
// import 'scss/layout-footer.scss'

export default {
  data () {
    return {
      author: 'Bowen'
    }
  },

  render (h) {
    return (
      // <div class="layout-footer">
      // 注意:若变量有连字符,那么变量必须写成 String 类型,如 'layout-footer'
      <div class={ className['layout-footer'] }>
        <span>Written by <a href="https://github.com/lbwa" target="_blank" >{this.author}</a></span>
      </div>
    )
  }
}

JavaScript 中深拷贝(开辟新的内存存储)对象的方法

拷贝方法的分类

深拷贝:开辟新的内存存储数据。
浅拷贝:没有开辟新的内存存储数据,仅仅复制指向数据的指针。

常用拷贝方法

  1. Object.assign(目标对象,要拷贝的对象)。
    注:当要拷贝的对象属性中存在引用(即对象的某个属性值是对象)时,仅仅复制引用(指针),此时针对此引用是浅拷贝,而不是深拷贝。

  2. 拓展运算符

Object.assign(obj0,obj1, obj2) 相同,后面的对象属性会覆盖前面的对象同名属性。

const obj = {...{ num: 10 }, ...{ num:20 }, ...{ num:30 }}
obj // { num: 30 }
  1. for...of 循环 Object.keys 和 Object.values ,其中 Object.keys(要拷贝的对象)得到所有键名组成的数组,Object.values(要拷贝的对象)得到所有键值组成的数组。

注:一般情况下不能直接使用 for..of 来遍历对象,除非部署 Iterator 接口。最佳实践是与 Object.keys() 搭配使用。

for (let key of Object.keys(someObject)) {
  console.log(key + ': ' + someObject[key]);
}

for...of 的类似循环 for...in 循环可用于遍历对象的键名,但是它也会枚举原型链上的属性,Object.keys 只枚举对象本身的属性

  1. JSON.parse(JSON.stringify(要拷贝的对象))——最简单的方法,但无法复制函数属性,会破坏原型链

  2. jQuery.extend(是否深拷贝,要拷贝的对象)

其中,特别注意的是 1,2 都是对对象的浅复制,即在被复制的对象中有指向另外的对象的指针,那么将会复制该指针!!

拓展:属性键名的遍历方法

ES6 一共有 5 种方法可以遍历对象的属性。

  1. for...in
    for...in 循环遍历对象自身的和继承的可枚举属性(不含 Symbol 属性)。

  2. Object.keys(obj)
    Object.keys返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)的键名。

  3. Object.getOwnPropertyNames(obj)
    Object.getOwnPropertyNames返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名。

  4. Object.getOwnPropertySymbols(obj)
    Object.getOwnPropertySymbols返回一个数组,包含对象自身的所有 Symbol 属性的键名。

  5. Reflect.ownKeys(obj)
    Reflect.ownKeys返回一个数组,包含对象自身的所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举。

以上的 5 种方法遍历对象的键名,都遵守同样的属性遍历的次序规则。

  • 首先遍历所有数值键,按照数值升序排列。
  • 其次遍历所有字符串键,按照加入时间升序排列。
  • 最后遍历所有 Symbol 键,按照加入时间升序排列。

参考

ECMAScript 6入门

函数防抖与节流

函数防抖

总结
意为在某段持续时间内,不断地触发事件,但只执行最后一次回调函数的调用。

实现:

/**
 * @param  {Function} fn     要实现函数防抖的原函数
 * @param  {Number}   delay  延迟时间
 * @return {Function}        添加防抖功能的包装函数
 */
// 最后一次调用总是会被执行
function debounce (fn, delay = 200, now = false) { // 延迟默认值 200ms
  let _timer = null // 匿名函数保持了对 _timer 变量的引用
  return function (...args) { // rest 参数,保存传入参数,用于向 fn 传递参数
    if (_timer) clearTimeout(_timer)
    // 箭头函数没有自己的 this 对象
    _timer = setTimeout(() => { // 这里的箭头函数中调用的 this 是外部匿名函数的 this
      fn.apply(this, args) // 指定 this 和参数,若不使用 apply 指定,那么 this 将指向 window
    }, delay)
  }
}

// 第一次立即调用后启用防抖
function debounce(fn, wait = 200, now = false) {
  let __timer = null
  return function (...args) {
    if (!__timer && now) {
      fn.apply(this, args)
    }
    
    if (__timer) clearTimeout(__timer)
    __timer = setTimeout(() => {
      fn.apply(this, args)
    }, wait)
  }
}

:一个易错点就是 setTimeout 中为箭头函数时,因为箭头函数自身是没有 this 对象的,它内部的 this 对象是外部的 this 对象,那么此时可直接调用匿名包装函数的 this(这也是箭头函数的一个典型应用)。但若 setTimout 中是非箭头函数时,必须先在外部引用匿名函数的 this,即 _that = this,然后再用 apply() 方法指定调用 fn 时的 this 对象。

以上示例中,debounce 函数修饰作用,用于定义一个闭包变量存储定时器和传入延迟载荷,返回的匿名函数也是 fn 函数的一个修饰,用于判断是否执行函数。其中 ...args 为 ES6 rest 参数,它定义了在调用匿名函数时,由传入的参数组成的数组(对于 arguments 伪数组而言)。

其中在 setTimeout 任务分发器中,是一个异步调用,那么必须指定调用 fn 的 this 和调用 fn 的包装匿名函数的传入参数。这是为了保证在使用防抖函数后调用 fn 与在没有使用防抖函数时调用 fn 的 this 对象和 arguments 对象一致。若不指定那么执行fn 时的 this 将指向 window,并且调用 fn 时无法正确传入 arguments 参数对象。

Vuejs 中的应用

// Vue.js 中使用函数防抖

created () {
  // watch 中真正的回调函数是 debounce() 返回的匿名函数
  this.$watch('query', debounce (newQuery => {
    this.$emit('queryChange', newQuery)
  }, 200))
}

在以上示例中,debounce() 表示了函数被调用,那么真正的回调函数是 debounce() 返回的匿名包装函数。因为 fn 的 this 与匿名包装函数的 this 是保持一致(使用 apply 指定的)的,那么 fn 的 this 此时是指向 Vue 实例组件的,rest 参数为由 newValueoldValue 组成的数组。

示例配置示例使用

函数节流

(结合 防抖函数 来实现节流函数的最后一次调用。)

意为在指定的单位时间内,只执行一次回调函数的调用。这是为了控制回调函数的一个最大的调用频率

/**
 * 用于限制 fn 函数在 period 时间段内只调用一次,即限制 fn 调用的频率
 * 示例中实现了首次和末次一定会被调用,中间调用被限定为一定频率
 *
 * @param {Function} fn 要被节流的函数
 * @param {number} [period=200] 被节流的时间段
 * @returns 一个匿名函数包装
 */
export function throttle (fn, period=200) {
  let _lastTime = null
  let _timer = null
  return function (...args) {
    const _nowTime = +new Date()

    _timer && clearTimeout(_timer)

    if (!_lastTime || _nowTime - _lastTime > period) {
      fn.apply(this, args)
      _lastTime = _nowTime
    } else {
      // 确保最后一次即使不满足 period 时间段,但仍会调用
      // 使用箭头函数来确保 this 不变
      _timer = setTimeout(() => {
        fn.apply(this, args)
      }, period)
    }
  }
}

以上示例是简单的函数节流实现,throttle() 函数作为一个函数包装器,传递目标函数和单位时间。
执行 throttle() 函数,返回的是另一个匿名包装函数,作用是限制 fn 的调用频率。判断是否是第一次执行,若不是第一次调用,则判断当前时间与上一次执行时的时间差,若大于指定的单位时间 wait ,则执行 fn 函数,使用 apply() 和 rest 参数,指定了调用 fn 时的 this 对象和 arguments 对象。并将执行时间 _lastTime 设置为当前时间 nowTime

函数防抖和函数节流对比

它们的目标都是为了防止过多的无意义函数调用。

函数防抖的目的是在多次连续触发事件时,在指定时间内(delay 参数)若再次触发调用回调函数,那么将忽略当前的函数调用,只有在经过指定时间内,没有再次触发事件时,才会真正的调用回调函数。

函数节流的目的是为了限制回调函数的最高执行频率的(比如 1s 内最多执行 2 次)。在单位时间内(wait 参数)最多执行一次回调函数调用。多余的回调函数调用将被忽略。

二者与普通回调函数调用的可视化对比如下:
debounceandthrottle

参考

函数防抖和节流

JavaScript 函数节流和函数去抖应用场景辨析

高级版函数防抖和节流

将依赖作为缓存对象来实现插件开发,避免直接引入依赖

vue-router

vue-rotuer 源码中,vuejs 的插件开发是依赖通过插件 install 方法 动态 传入的 Vue 对象。这是为了避免单独 import Vue 将导致插件直接依赖 Vuejs,进而导致 package 包含 Vuejs,即插件体积增大。

// src/index.js
import { install } from './install'

export default class VueRouter {
  // ...
}
// 定义插件的 install 方法,在作为模块引入插件时,必须调用 Vue.use(VueRouter) 来向 install 方法动
// 态传入 Vue 对象,使得插件可以使用 Vue 对象
VueRouter.install = install

// 全局环境自动调用 Vue.use(VueRouter)
if (inBrowser && window.Vue) {
  window.Vue.use(VueRouter)
}

通过在插件的 install 模块中建立一个缓存容器来缓存作为参数被传入的 Vue 对象。此时, Vue 是作为参数 动态 传入,并不是作为函数中的静态变量而存在。那么仅在调用 install 方法时, Vue 对象才会被传入。那么也就避免了在插件 install 函数中直接依赖 Vue 对象。同时也并不影响插件在被调用时对 Vue 对象的依赖。

// src/install.js

// 缓存容器,export 之后可用于其他非 install.js 的模块调用 Vue 对象,如 src/history/base.js
export let  _Vue

// 通过参数形式来避免 Vue 被 install 方法直接依赖,此举可避免插件 package 打包 Vue
export function install (Vue) {
  if (install.installed && _Vue === Vue) return
  install.installed = true

  _Vue = Vue
  // ...
}

总结

纵观 vuejs 的插件开发原理,无论是在以后自己开发 vuejs 插件还是其他库,都可利用 将依赖转变为参数 的方式来避免 package 体积增大。即避免了模块对其他模块的直接依赖。这个思路个人认为比较适合插件开发,因为它的依赖动态引入原理极大地限制了插件的 dependcies

Reference

typescript collections

使用技巧

  • 定义对象的任意键名的值为指定类型
// key 为变量,类型为 string,对应的属性值为 number 类型
// 即 option 的所有属性键值均为 number 类型,除非形如 age 键名,另外特别指定类型
interface option {
  [key: string]: number
  age: number
}
  • 泛型,指不在 定义时 确定类型,而在 运行时 确定类型。泛型的出现允许我们跟踪函数内的变量类型信息。
// 示例中泛型表示输出与输入的值类型必须一致
function createFn<T>(arg: T): T {
  return arg
}

// 使用
// 方法 1 :显式地指出泛型的运行时类型。
// 调用时确定了类型 string ,即传入的参数必须为 string
let output = createFn<string>('hello')
// 方法 2 : 利用类型推论使用泛型
// 编译器将根据参数类型来设置泛型的运行时类型。
let output = createFn('hello')

:在泛型的类型未确定时,不能调用有关参数的属性(即泛型约束)。

function loggingIdentity<T>(arg: T): T {
  // 泛型在被调用时,可能是任何类型,而此时没有任何地方表明这个可能是任何类型的变量 arg
  // 具有 length 这个属性,故将报错
  console.log(arg.length)
  return arg
}
function loggingIdentity<T>(arg: T[]): T[] {
  // 泛型的类型不影响 arg 是数组,故可正常运行
  console.log(arg.length)
  return arg
}

Redux 基本核心概念

import { createStore } from 'redux'

/**
 * This is a reducer, a pure function with (state, action) => state signature.
 * 这是一个 reducer,一个形如 (state, action) => state signature 的 **纯函数**。
 * It describes how an action transforms the state into the next state.
 * reducer 描述例如一个 action 如何将一个 state 转换为一个新的 state。
 *
 * The shape of the state is up to you: it can be a primitive, an array, an object,
 * state 的结构取决于开发者:它可以是原始类型值,数组,对象
 * or even an Immutable.js data structure. The only important part is that you should
 * 或者甚至是一个 Immutable.js 的数据结构。唯一重要的点是开发者
 * not mutate the state object, but return a new object if the state changes.
 * 不应该修改 state 对象,而是当 state 变化时,返回一个新的 state 对象。
 *
 * In this example, we use a `switch` statement and strings, but you can use a helper that
 * follows a different convention (such as function maps) if it makes sense for your
 * project.
 */
function counter(state = 0, action) {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1
    case 'DECREMENT':
      return state - 1
    default:
      return state
  }
}

// Create a Redux store holding the state of your app.
// 创建一个 Redux store 来托管你的 app 的 state
// Its API is { subscribe, dispatch, getState }.
let store = createStore(counter)

// You can use subscribe() to update the UI in response to state changes.
// 你可以使用 subscribe() 来根据 response 更新 state
// Normally you'd use a view binding library (e.g. React Redux) rather than subscribe() directly.
// 通常情况下,你最好使用一个视图绑定库,而不是直接使用 subscribe()。
// However it can also be handy to persist the current state in the localStorage.
// 然而它也可以方便地在 localStorage 中持久化当前 state

store.subscribe(() => console.log(store.getState()))

// The only way to mutate the internal state is to dispatch an action.
// 修改内部 state 的唯一方式是 dispatch 一个 action
// The actions can be serialized, logged or stored and later replayed.
// action 可被序列化,打印,存储或者稍后重放
store.dispatch({ type: 'INCREMENT' })
// 1
store.dispatch({ type: 'INCREMENT' })
// 2
store.dispatch({ type: 'DECREMENT' })
// 1

而不是直接修改 state,你可以指定任意修改 objectmutations,我们叫这种 mutations 叫做 actions。然后你写下一个叫做 reducer 的**纯函数**用于决定每一个 actions 是如何转换整个 appstate

在一个典型的 Redux app 中,只有唯一一个 storeroot reducing 函数(即 root reducer)。随着你的 app 的增长,你可以将 root reducer 拆分为分布在 state 树不同部分的更小的 reducer。这就像在一个 React app 中只存在一个根组件,但它是由许多更小的组件组合而成的。

这个架构似乎看起来对于一个 counter 应用是有点杀鸡用牛刀(overkill)了,但是这种模式的绝妙之处在于它可以很好的拓展至一个庞大并且复杂的 app。它也是非常强劲的开发者工具,因为可追踪每个引起 mutation 的源头 action。你可以记录用户绘画并且通过重放每一个 action 来重现用户会话。

vuejs router collections

浏览器默认跳转行为

现象

vuejs 应用中,有些链接会刷新控制台,有些不会,即有部分刷新控制台的链接执行了多页面跳转。这引起了自己的思考。

多页面跳转的弊端有:

  1. <a> 标签执行多页面跳转时,无法在跳转过程中处理过场动画的问题。用户体验差,页面出现冻结。

  2. 多页面跳转将会刷新整个页面,那么将导致无关组件的不必要的重建刷新。

本质

vue-router 处理了页面的跳转,阻止了浏览器默认的多页面跳转行为。达到了 更新特定组件的目的(原 <router-view/> 区域)。而浏览器的默认行为会刷新 所有 组件。

解决方案

  • 编程式导航

手动阻止浏览器的默认跳转行为,调用 this.$router.push() 执行页面跳转。

<a class="link-item"
  :href='`/${destination}`'
  @click.stop.prevent="navigate"
>{{content}}</a>
navigate (evt) {
  const href = evt.target.getAttribute('href')
  this.$router.push(href)
}
  • vue-router 全权处理页面跳转

模块中的 <a> 标签亦可替换为 <router-link> 标签,这样会在 vue-router 内部调用 this.$router.push() 该方法,并阻止链接的默认多页面跳转行为。

<router-link class="link-item" :to='`/${destination}`'>{{content}}</router-link>

总结

组件中的跳转行为是一个容易忽视的对性能有影响的地方。只有阻止了浏览器的默认多页面跳转行为才能将组件的刷新范围降到最低。否则浏览器的默认多页面跳转行为将刷新整个页面。

DOM and CSS optimization

Redirect -> #20 HTML & CSS collections

十分推荐 查看 Google developers渲染性能 相关的 web 基础。

避免重排和重绘

Chrome 中如何详细地查看 浏览器进行的布局更改

DOM tree 与 CSSOM tree 构建并合并为渲染树要经历以下阶段

1

DOM tree 与 CSSOM tree 需要经历 js -> style -> layout -> paint -> composite 五个阶段来响应样式或 DOM 树的变化

1

现阶段常见属性只有 opacitytransformfilter 可避免其中最耗时的 layoutpaint 阶段,他们的改变只需要经历 composite 阶段(Google developers 所推荐的 实现动画 的方法)。

1

具体可见由 google 编写的 csstriggger,之后结合调试来避免不必要的重排和重绘。

一般规律

  • layout
    涉及 DOM 操作,如尺寸 width / height,位置 top:0 等,边距 margin 等样式均会触发 layout 进而导致 repaint 重绘。

  • paint
    涉及 DOM 节点的颜色 color,阴影 box-shadow,背景 background 等。

  • composite
    目前常见的 css 属性中只有 opacitytransformfilter 这三个属性的修改只需要进行 composite 操作。

Notice

多结合 Chrome Dev Tools 中的 performance 选项卡来查看重排和重绘的具体细节,还有 console drawer 中的 more tools -> Rendering 可查看具体哪些行为会引起重绘。

reference

开发时 SSR server 端静态资源的路径处理

缘由

在 SSR server 中可通过 ajax 请求 client server 获得渲染信息。

// ctx 为 koa 请求生成的 context 对象,它作为中间件的接收器,这里即作为渲染上下文使用
const handleSSR = async (ctx) => {
  // ...

  // 获取客户端构建清单,即客户端 bundle(另一个 server 的数据)
  // vue-ssr-client-manifest.json 是 webpack.dev.config 中 VueClientPlugin 默认生成文件名
  const clientManifestResp = await axios.get(
    // http://koajs.com/#context
    // 因为 Koa 将 Node request 对象封装至 ctx 上,所以此处 ctx 对象被赋予了请求地址,即此时存在了 ctx.path(ctx.request.path 的别名)
    'http://127.0.0.1:8080/public/vue-ssr-client-manifest.json'
  )

  // 在 await resolved 后,即得到 clientManifestResp,继续执行
  const clientManifest = clientManifestResp.data

  // ...
}

因为 SSR server 中 HTML 的 script 地址是渲染自 client server 中 HTML 的 script 地址(server-render.jscontext.url 和插入的 scripts 标签配置),那么 SSR server 在访问 client server 的静态资源 'a.js' 时的地址也是和 client server 访问 a.js 的地址相同,即 '/自定义基路径/a.js',那么 SSR server 在访问 'a.js' 时,SSR server 实际访问的地址也是 '/自定义基路径/a.js',那么自然 SSR server 无法正确访问到 client server 静态资源 a.js 的正确地址。

(以上隐含了一个服务端没有跨域限制的知识点)

配置:

// SSR server 渲染请求地址
const context = { url: ctx.path }

// ...

const html = ejs.render(template, {
  // ...

  /**
   * 1. context.renderScripts() 返回引导客户端应用所需的 <script> 标签
   * 2. 需要 clientManifest,即客户端 bundle
   * 3. 此处插入的地址将与 client server 插入的 script 标签地址一样
   */
  scripts: context.renderScripts()
})

解决方案:

  1. webpack.server.config.js 设置代理,即 SSR server 设置代理。

  2. 直接设置补全 client server 访问静态资源地址,即将 '/自定义基路径' 补全为 'http://127.0.0.1:8080',那么此时不管是 client server 还是 SSR server 访问 a.js 的地址都是一个源下的静态资源,即 client server 下的静态资源。(点我查看示例配置,其中 webpack output 地址必须引用该配置中的 assetPublicPath)

Git collections

Steps

以下介绍如何删除 git 本地和远程仓库中的误上传的敏感文件及其 commit 记录。

# 在仓库根目录下
cd <Your repo name>

# 1. 删除指定文件(或文件夹)在 commit 中的记录,本质是重写了仓库所有相关 commit 历史记录。
git filter-branch --force --index-filter \
'git rm --cached --ignore-unmatch _PATH-TO-YOUR-FILE-WITH-SENSITIVE-DATA_' \
--prune-empty --tag-name-filter cat -- --all

# 2. 强制同步远程仓库为重建后的 commit 历史记录
git push origin --force --all

# 3. 强制同步远程仓库的 tags,删除 tags 中的敏感数据记录
git push origin --force --tags

Extension

# 删除本地名为 tag name 的标签
git tag -d <tag name>

# 将本地被删除的标签同步至远程仓库,即以空标签覆盖远程记录,以达到删除远程标签的目的
git push origin :refs/tags/<tag name>
# 或者
git push --delete origin <tag name>

Reference

Github help

Git official handbook

How to delete a git remote tag ?

浏览器读取了不存在的dist目录中的build.js

疑惑:执行npm run dev时目录中并没有生成dist目录,但浏览器却读取了根目录下的dist目录中的build.js(webpack打包的输出js文件——output选项)

package.json配置npm scripts如下:

"scripts": {
  "dev": "webpack-dev-server --inline --hot",
  // ...
}

webpack-dev-serverreadme.md

It uses webpack-dev-middleware under the hood, which provides fast in-memory access to the webpack assets.

而在webpack-dev-middleware中的readme.md

No files are written to disk, rather it handles files in memory

以上翻译过来就是在创建middleware时,所有的文档操作都在内存中进行,整个过程对硬盘没有读写。而webpack-dev-server又是建立在web-dev-middleware的基础之上,所以,在整个使用1. webpack打包文件,2 .创建server的过程中,webpack打包后的文件只存在于内存中,硬盘中是不会创建dist文件目录的,更看不到编译后的bundle.js文件。

如何在 markdown 中引用 svg ?

方法一

  • https:rawgit.com

  • 代码

![http-bw][http-bw]
[http-bw]:https://rawgit.com/lbwa/lbwa.github.io/dev/source/images/post/http-protocol/http-bw.svg
  • 结果
    http-bw

方法二

  • https://rawgithub.com

  • 代码

![http-bw][http-bw]
[http-bw]:https://rawgithub.com/lbwa/lbwa.github.io/dev/source/images/post/http-protocol/http-bw.svg
  • 结果
    http-bw

方法三

  • https:raw.github.com 的 URL 结尾处添加 ?sanitize=true

  • 代码

![http-bw][http-bw]
[http-bw]:https://raw.github.com/lbwa/lbwa.github.io/dev/source/images/post/http-protocol/http-bw.svg?sanitize=true
  • 结果
    http-bw

方法四

  • 在当前 markdown 文件为起点使用相对位置链接 svg
+
|
+---+file-name
|        |
|        +---+ some.md
|        |
+        +---+ target.svg
  • 代码
![http-bw](./target.svg)
  • 注:只对存储 host 路径为 https://github.comsvg 有效。

方法五

  • https://<username>.github.io

svg 链接地址 host 修改在 github.io 名下,默认为 master 分支。

  • 代码
![http-bw][http-bw]

[http-bw]:https://lbwa.github.io/images/post/http-protocol/http-bw.svg
  • 结果

http-bw

reference

webpack optimization option

webpack chunk optimization

Chunk loading

Chunk 优化原理

如果两个 chunk 包含相同的 modules,那么这些相同的 modules 将会合并为一个。这种情况下可能导致 chunks 有多个 parents

如果一个 chunk 的所有 parents 都有一个相同的 modules,那么该 modules 将被从该 chunk 中移除。

如果一个 chunk 包含所有其他 chunk 的所有模块,那么该 chunk 将被存储,因为该 chunk 能适用于多个 chunk

Chunk types

  • Entry chunk

包含一串 runtime (即运行时)的 modules,如果 chunk 包含 module 0 那么 runtime 会立即执行该 chunk。如果不是,那么等待包含 module 0chunk 执行完成之后再执行该 chunk(每次打包都会生成一个包含 module 0chunk )。

  • Normal chunk

一个正常不包含 runtimechunk。该 chunk 的结构依赖于 chunk 的加载算法。

  • Initial chunk (non-entry)

一个初始 chunk 也是一个 normal chunk。不同之处在于初始 chunk 的优化是非常重要的,因为他被计算在初始加载时间内(如,入口 chunk

  • webpack 默认启用优化情形
  • 应该分离 modules 的情形可查看官方示例

optimization.runtimeChunk

official site

即分离仅包含 runtime 的代码块,形成单独的一个 chunk,命名为 runtime chunk,它与应用源代码是引用关系。那么可利用该特点进行浏览器常缓存。

在应用源代码发生改变时,其 contentHash 发生变化,但是此时的 runtime chunk 因与源代码非包含关系,那么 runtime chunk 内容没有发生变化,也就不会影响 runtime chunk 的浏览器常缓存。

一般 runtime chunk 其中是 webpack 对其各个 chunk 的引用机制,它用于处理不同的 chunk 的执行关系,即它是直接与 webpack 相关的代码,而与业务源代码无关的代码。

module.exports = {
  // ...
  optimization: {
    runtimeChunk: {
      name: 'runtime'
    }
  }
}

optimization.runtimeChunk 默认为 false。那么每一个 entry chunk 都将包含 runtime

optimization.splitChunks

webpack 默认优化配置如下:

以下值均为默认值

module.exports = {
  //...
  optimization: {
    splitChunks: {
      /**
       * 指定需要优化的 chunk。 string | Function
       * 
       * 1. 'all' 即指明为所有 chunk,即可理解为表示所有可在异步和非异步 chunk 之间
       *    共享的模块。
       * 2. 'async' 异步的 chunk,即分离所有异步 chunk,与 import ('./a') 语法配合
       *    使用。
       * https://webpack.js.org/plugins/split-chunks-plugin/#defaults-example-1
       * 3. 'initial' 同步的 chunk
       * 4. 可被 `splitChunks.cacheGroups` 中的同名选项覆盖
       * 5. 可结合 HtmlWebpackPlugin 的配置来注入所有生成的 vendor chunks。
       */
      // 即默认地,webpack 将异步 chunk 单独分离出来。
      chunks: 'async',

      // 最小 chunk 体积。 number
      minSize: 30000,

      // 分离前必须共享的最小 chunks 数。 number
      minChunks: 1,
      
      // 按需加载时并行请求的最大数。number
      maxAsyncRequests: 5,

      // 入口处的最大并行请求数 number
      maxInitialRequests: 3,

      // 默认情况下,webpack 以来源和 chunk 名来命名生成的 chunk
      // 例如 vendors~main.js
      // 该选项允许你指定生成名字的连字符
      automaticNameDelimiter: '~',

      // 分离生成的 chunk 的名字 boolean: true | Function | string
      // 默认基于 chunk 的键名和 cacheGroups 的键名生成
      // 可提供一个 string 或 Function 自定义名字
      // 可被 `splitChunks.cacheGroups` 中的同名选项覆盖
      name: true,

      /**
       * 缓存组,即应该被浏览器常缓存的第三方库。 object
       * 
       * 1. 其中的选项可继承或覆盖外部任意 `splitChunks.*` 选项,除了 `test`, 
       *    `priority`, `reuseExistingChunk` 只能在该选项内配置
       * 2. splitChunks.cacheGroups.default: false 即可关闭
       */
      cacheGroups: {
        // 一个缓存组,在其内部未指明 name 的情况下,使用该键名命名生成的 chunk
        vendors: {
          /**
           * 指定哪些 modules 应该被该缓存组缓存。 RegExp | string
           * 
           * 1. 未指定时,将选择所有 modules。
           * 2. 可选择一个绝对路径的资源或 chunk 名,当一个 chunk 被选定时,其中所有
           *    的module 同样将被选定。
           * 3. 在搭配形如 chunk: 'all' 选项可选择在被选中的 modules 中哪些
           *    modules 应该被缓存
           */
          test: /[\\/]node_modules[\\/]/,

          // 一个 module 可属于多个缓存组,但 optimization 会将 module 优先缓存于高
          // 优先级的缓存组 number
          priority: -10
        },
        // 默认的缓存组
        default: {
          // 分离前必须共享的最小 chunk 数,可覆盖外部同名选项。number
          minChunks: 2,

          // 如前文所述,遇到可加入多个缓存组的 module 时,优先级将低于 vendor 缓存组
          priority: -20,

          // 若符合默认条件组的 module 已经被最近从主 bundle 分离出的 chunk 包含,那
          // 么重用这个 module,而不是生成新的 chunk。这会影响最终生成的 chunk 的文件名。
          reuseExistingChunk: true
        }
      }
    }
  }
}
/**
 * 应该分离 modules 的情形
 * https://webpack.js.org/plugins/split-chunks-plugin/#defaults-example-1
 * 
 * 1. chunk 包含了来自 node_modules 的 modules
 * 2. 1 的 modules 大小超过 30 kb,若 chunk 将其包含在内将影响首轮加载
 * 3. 不影响初始页面加载的 modules
 * 4. 不经常修改的代码,即应该被浏览器常缓存的代码,如 1 中的第三方库。
 * 
 * https://webpack.js.org/plugins/split-chunks-plugin/#defaults-example-2
 * 
 * 5. 可被不同页面共享的 chunk
 */

// 创建一个 `commons` chunk,它包含所有入口共享的所有代码
// 此举将增加初始 bundles 的体积,只在需要动态引入时推荐使用
module.exports = {
  //...
  optimization: {
    splitChunks: {
      cacheGroups: {
        // 一个缓存组,仅当内部未指定 name 选项时,名字以键名指定
        commons: {
          name: 'commons',

          // 缓存所有初始的 modules,即排除所有异步 modules,将动态导入的文件和非动态导入的文件
          // 分别打包
          chunks: 'initial',
          minChunks: 2
        }
      }
    }
  }
}

// 创建一个 `vendors` chunk,它包含所有应用中需要的第三方库
module.exports = {
  //...
  optimization: {
    splitChunks: {
      cacheGroups: {
        commons: {
          // 指明为 node_modules 被依赖的 modules
          test: /[\\/]node_modules[\\/]/,

          // 名为 vendors 的缓存组,键名 commons 仅在未指明 name 时生效。
          name: 'vendors',

          // 将所有符合 test 选项的 modules 缓存
          chunks: 'all'
        }
      }
    }
  }
}

不同浏览器操作引用文档图片的Canvas元素

结论:在chrome中操作Canvas元素(其中包含对文档图像的引用)时,要保证在脚本文件执行结束时,图像已经加载完成。在 firefox 和 IE11 中不会出现此问题

情境:
在chrome中,当脚本文件中有操作引用文档图片的元素时,若把应引用外部文件的script元素放到head元素中,即如下所示:

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Document</title>
  <script defer type="text/javascript" src="1.js"></script>   //defer 立即下载外部文件,同时继续解析渲染文档,在完成文档渲染完成后执行外部脚本文件
</head>
<body>
  <canvas id="canvas" width="1000" height="1000">A drawing of something.</canvas>
  <img src="smile.gif" alt="img">
</body>
</html>

Chrome效果图:
img

此时,Chrome瀑布图:
chromewaterfall-1

首先,<script>元素中async属性表示,开始下载脚本文件,并继续解析文档,当脚本文件下载完成时尽快执行脚本文件,defer属性表示开始下载文档,并继续解析文档,延迟脚本执行,直到文档解析渲染完成才开始执行脚本文件。而在客户端JavaScript时间线中,当文档解析器遇到没有async和defer属性的<script>元素时,它把这些元素添加到文档中,然后执行行内或外部脚本,这些脚本会同步执行,并且在脚本下载(如果需要)和执行时解析器会暂停(即阻塞文档解析)。
由瀑布图可知,浏览器首先加载文档,当解析到<script>元素引用了外部文件1.js时,因为有defer属性的存在,所以浏览器异步下载脚本,但延迟执行,直到文档渲染解析完成后再执行脚本文件,同时,继续解析渲染文档。当解析到 img 元素时,浏览器开始下载外部图片,并继续渲染和解析文档。此示例出现了一种情况是,当文档解析完成,且脚本执行完毕,但是此时的图片仍在下载中,所以导致出现了canvas元素显示错误。此时浏览器并不会报错

将之前代码修改如下即可正常显示引用文档图片的元素:
HTML

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Document</title>
</head>
<body>
  <canvas id="canvas" width="1000" height="1000">A drawing of something.</canvas>
  <img src="smile.gif" alt="img">
  <script type="text/javascript" src="1.js"></script>              // 放在<body>尾端
</body>
</html>

JavaScript

let drawing = document.querySelector('#canvas');
let context = drawing.getContext('2d');

let img = document.images[0];
pattern = context.createPattern(img,'repeat');
context.fillStyle = pattern;
context.fillRect(10,10,150,150);

chrome效果图:
img
此时的瀑布图如下:
chromewaterfall-2
此时,由图可得出一般性结论,在chrome中,只有当图片下载完成的时间早于脚本文件执行完成的时间(与他们的耗时无关,图片下载耗时可能仍旧大于脚本的耗时),就不会出现之前的问题。

PWA & Service Workers collections

缓存

缓存策略应该是 networkFirst 网络请求优先,而不是 cacheFirst 缓存优先。这样可以保证在每次打开页面时首先请求网络验证缓存是否需要更新。否则只要本地存在缓存时,都不会请求源服务器,即使源服务器已经更新,client 端仍会优先使用缓存。所以缓存策略应该是 network first

workbox 为例:

importScripts('https://storage.googleapis.com/workbox-cdn/releases/3.4.1/workbox-sw.js')

const workboxSW = new self.WorkboxSW({
  "cacheId": "some-data",
  "clientsClaim": true,
  "directoryIndex": "/"
})

workboxSW.precache([
  {
    "url": "/_data/app.6866b.js",
    "revision": "c708b23519be872c35fa1d141ebbc30f"
  },
  // ...
])

workbox.routing.registerRoute(
  new Reg('/.*'),
  workbox.strategies.netWorkFirst({}),
  'GET'
)

数字和日期便捷格式化、模仿placeholder、防止重复提交、解决DOM取值无效

1.在选项表单控件中模仿placeholder属性

目标效果如下:
img
在选项表单中通过设置 disabled selected hidden 三个属性可以达到模拟placeholder属性的效果,默认显示,选择时消失并不可回选。
实际应用:form.html

<select class="gender" name="gender">
      <option value="Your Gender" disabled selected hidden>Your Gender</option>
      <option value="Male">Male</option>
      <option value="Female">Female</option>
      <option value="Secret">Secret</option>
</select>

2.防止重复提交

如何防止用户操作的不必要的多次函数运行,或者多次不必要的表单提交。个人的做法是在触发监听程序之后,移除监听器,待监听程序执行完毕之后,再添加之前删除的监听程序。

3.非number类型处理为number类型

因为Number()方法处理整数的规则过于复杂,且'1a'这类值无法转换为number类型。一般处理整数的方法是parseInt(),但是空字符串使用parseInt()方法返回NaN(当两种方法都无法按自己的规则返回数值时,都返回NaN),此时再使用Number()方法,会返回0。

补充:数字格式化

Number.prototype.toLocaleString( [ locales [, options ] ] ) 可将数字格式化为特定格式字符串,如补0。控制位数,是否有逗号(分隔符),添加货币符号等等。

注:该方法最终返回的结果是字符串。使用补0时,第一个参数 locales 要设置(如:'zh')。

const num = 2333333;
num.toLocaleString('zh', { style: 'decimal' }); // 2,333,333
num.toLocaleString('zh', { style: 'percent' }); // 233,333,300%
num.toLocaleString('zh', { style: 'currency' });    //报错
num.toLocaleString('zh', { style: 'currency', currency: 'CNY' }); // ¥2,333,333.00
num.toLocaleString('zh', { style: 'currency', currency: 'cny', currencyDisplay: 'code' }); // CNY2,333,333.00
num.toLocaleString('zh', { style: 'currency', currency: 'cny', currencyDisplay: 'name' }); // 2,333,333.00人民币
num.toLocaleString('zh', { minimumIntegerDigits: 5 }); // 02,333.3
//如果不想有分隔符,可以指定useGrouping为false
num.toLocaleString('zh', { minimumIntegerDigits: 5, useGrouping: false }); // 02333.3
num.toLocaleString('zh', { minimumFractionDigits: 2, useGrouping: false }); // 2333.30

// 控制有效数字
const num = 1234.5;
num.toLocaleString('zh', { minimumSignificantDigits: 6, useGrouping: false }); // 1234.50
num.toLocaleString('zh', { maximumSignificantDigits: 4, useGrouping: false }); // 1235

补充:日期格式化

Date.prototype.toLocaleString( [ locales [, options ] ] ) 可将数字格式化为特定格式字符串

const date = new Date();
date.toLocaleString('en', { weekday: 'narrow', era: 'narrow' }); //W A
date.toLocaleString('en', { weekday: 'short', era: 'short' }); //Wed AD
date.toLocaleString('en', { weekday: 'long', era: 'long' }); //Wednesday Anno Domini

const date = new Date();
date.toLocaleString('zh', { timeZoneName: 'short' }); //2018/4/5 GMT+8 下午7:18:26
date.toLocaleString('zh', { timeZoneName: 'long' }); //2018/4/5 **标准时间 下午7:18:26

const date = new Date();
date.toLocaleString('zh', { year: 'numeric',  month: 'numeric',  day: 'numeric',  hour: 'numeric',  minute: 'numeric',  second: 'numeric', }); //2018/4/5 下午7:30:17
date.toLocaleString('zh', { year: '2-digit',  month: '2-digit',  day: '2-digit',  hour: '2-digit',  minute: '2-digit',  second: '2-digit'  });   //18/04/05 下午7:30:17

const date = new Date();
date.toLocaleString('en', { month: 'narrow' }); //A
date.toLocaleString('en', { month: 'short' }); //Apr
date.toLocaleString('en', { month: 'long' }); //April

4.输入文本框取值

对input输入框的取值语句,不能放在全局作用域中,必须放在函数作用域内,由函数调用来触发取值。
以下为示例:

let trigger = function(event) {
  let hour = document.querySelector('#myHour').value;
  let min = document.querySelector('#myMin').value;
  ...要执行的操作
}

若hou和min的语句放在全局作用域中,那么在文档加载外部资源执行语句的时候就已经取值。此时,并不是用户输入值。
将hou和min语句放在函数trigger中时,在加载外部资源时,只要脚本内没有调用该函数的语句,那么trigger中的语句将不会执行。此时,可根据自己的需要调用trigger函数取得用户输入值。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.