Code Monkey home page Code Monkey logo

blog's People

Contributors

spivet avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

blog's Issues

前端自动化部署的几种不完全姿势

手动 FTP

这应该是最基础的姿势

执行类似 npm run build 的命令

手动打开 FileZilla 之类的 FTP 软件

手动连接到服务器

手动找到对应目录

手动删除旧数据

手动上传新数据

自动化脚本

如果你们的部署是十天半个月一次,个人觉得上面的手动操作是完全可以接受的,除去构建过程,其余步骤也就两三分钟的事。

但如果像我们一样,一天最多可能会发布十多次到测试环境的时候,那上面的步骤就让人有点烦躁了。

作为一个有理想有追求的软件工程师,这个时候我们就要发挥自己的才能解放自己了,方式就是自动化脚本。

自动化脚本就是写代码,既然是代码,那实现方式就有点多了,这里介绍几种我尝试过的:

1. node 脚本

使用 node 相关的 ftp 库,比如 ssh2-sftp-client,通过这些库,可以连接到服务器,并进行删除,上传,下载等操作。

但这种方式有个缺点,得递归操作,意思就是上传和下载文件得一个个文件操作,遍历文件夹。

我写了一半就放弃了,所以这里就不贴代码了,有兴趣的朋友自己去尝试。

2. gulp 流

怀念以前配置 gulp 自动化脚本的日子,直到现在仍然觉得 gulp 比 webpack 好用,唯一差的就是对模块的打包处理。

废话不多说,直接贴代码:

// package.json

"scripts": {
  "deploy-test": "vue-cli-service build --modern --mode development && gulp",
},
// gulpfile.js

const gulp = require('gulp')
const sftp = require('gulp-sftp')

gulp.task('default', () => {
    return gulp.src('./dist/**/*')
        .pipe(sftp({
            host: '10.0.23.20',
            port: 22,
            user: 'root',
            pass: '123456',
            remotePath: '/remote/path/'
        }))
})

在需要发布的时候,执行 npm run deploy-test 命令就行。

这个的缺点是需要全局安装 gulp,另外我还没发找到删除旧文件的方法,虽然没有删除的必要,因为 vue-cli 3 开发模式生成的文件没有 md5 戳,上传文件会直接覆盖。

3. shell 脚本

如果不想额外安装任何工具,而且还有删除旧文件的强迫症,那就使用 shell 脚本吧:

# deploy-test.sh

#!/bin/bash
#sftp执行
npm run build-test
sftp [email protected] <<EOF
cd ../remote/pathname
rm /remote/pathname
put -r dist/.
bye
EOF

username 是服务器账号,10.0.0.0 这个是你们的服务器 IP

mac 用户直接在 terminal 工具里执行 bash deploy-test.sh 命令就行,Windows 用户可以使用 git bash,大家在安装 Git 的时候应该都有选安装 git bash 吧。

另外这种方式也有个问题,就是每次连接要输入密码,解决这个问题的最简单方式就是在服务器先部署一个公钥,用你使用 git 时生成的那个就可以。

将公钥部署到服务器上:

ssh-copy-id -i ~/.ssh/id_rsa.pub [email protected]

测试免密码登录:

服务器端自动构建

上面的方法都只适合测试环境的开发部署,大家都可以操作,删除修改服务器文件,就是把服务器搞挂了都无所谓。

但正是环境也这样就很危险了,而且正式环境还可能会需要部署多台服务器,这种情况下就要求我们采用更安全可靠的部署方式。

1. jenkins

关于 jenkins 的使用网上有很多文章,这里不介绍具体使用方法,篇幅太长,不了解的同学百度一下 jenkins 自动部署就行。

2. 服务器厂商自带服务

服务器厂商自带服务其实很多也就是把 jenkins 部署到了服务器上,不过提供了其它一些额外的功能。华为云,阿里云都有自家对应的产品,其它云服务没用过,不过近期打算用用腾讯云,到时候可以看看。

这里介绍一下阿里云服务产品,华为云忘记了。

CodePipeline,方便快捷,适合小团队或者不是特别大型复杂的应用。

云效,一站式企业协同研发云,提供从“需求->开发->测试->发布->运维->运营”端到端的协同服务和研发工具支撑。适合研发流程严谨的多人团队和较为复杂的项目,现在还改了收费方式,30人以下免费,非常适合创业团队。

文档就大家自己去看了,免费推广了应用就不免费写教程了,刚开始配置使用的时候还是有些坑,当时也是花了一两天时间才跑起来。

要写的话,篇幅有点长,空了再补,感兴趣的朋友可以亲自尝试踩踩坑,然后写篇文章分享出来,到时候可以给我个链接,我直接放进来 ~ ^^ ~

基于Vue与flask的前后端分离开发

公司的项目是以后端为主的传统MVC模式构建的,处于业务发展需要,以后可能需要给别人提供API,而且移动端和小程序也要上线,所以为了以后的便利和代码复用,现在需要采取前后端分离的模式。

技术选型

之前是用Python作为后端语言,Flask框架,view层采用 Jinja2 模板引擎。现在整体业务已经成型,所以后端是没法再变的,依然采用Python + Flask,能变的只能是 view 层,将 view 层完全抽离出去,全部由前端负责。

前端最终采用的是 Vue 全家桶,原因无它,对于这种小体量公司,用 Node 作为中间层这种模式,我深信如果踩到坑,以后填起来会很痛苦,而且也没法发挥出 Node 做中间层的优点。

架子搭建

基础结构

project
    | -- backend       // backend 文件夹作为后端文件的根目录
    | -- dist          // dist 文件夹里放的是前端打包的最终代码,也就是 npm run build 生成的代码
    | -- frontend      // frontend 自然就是存放前端开发环境的代码了。

我没有把前端打包生成好的代码放到后端的 static 或者 template 文件夹下,主要原因是为了前后端能够分开发布,你肯定不想陪着后端人员一起,大半夜的守在电脑面前部署代码吧。

前端环境

前端使用的是 Vue.js 一整套的东西,具体使用和配置直接看官网就好了。

进入到 frontend 文件夹,然后在命令行工具里,通过 vue-cli 执行:

vue init webpack

然后安装好我们前端开发时需要的包,目录结构就变成了下面的样子

image

不过生产环境的构建配置还需要做点小修改,打开 config 文件夹里的 index.js 文件,并将 build 时候的配置:

build: {
    // Template for index.html
    index: path.resolve(__dirname, '../dist/index.html'),

    // Paths
    assetsRoot: path.resolve(__dirname, '../dist'),

修改为:

build: {
    // Template for index.html
    index: path.resolve(__dirname, '../../dist/index.html'),

    // Paths
    assetsRoot: path.resolve(__dirname, '../../dist'),

这样在执行 npm run build 进行生产打包时,文件就能输出到我们想要的位置了。

后端环境

Python 是最新的 V3.6,这个不说了,大家自己去官网下载安装就行了。

打开 cmd,进入到 backend 目录:

image

Python 不像 Node.js,通过 NPM 安装包时,只需要一个参数就能区分全局和当前项目环境,安装在当前项目环境的包永远不会影响其它的项目环境。但是 Python 不行,所以为了各个项目之间的环境独立,我们需要安装 virtualenv,把每个项目都放在一个封闭的虚拟环境中,这样项目彼此间就不会影响了。

安装 virtualenv:

pip install virtualenv

创建虚拟环境:

virtualenv venv

venv 是虚拟环境的名字,所以只要你喜欢,换成什么都可以。

进入虚拟环境:

venv\Scripts\activate

image

退出虚拟环境:

venv\Scripts\deactivate

安装 Flask:

pip install Flask

安装 flask-cors,用来解决开发时的跨域问题:

pip install flask-cors

安装 pylint,用来检查代码:

pip install pylint

安装 yapf,用来格式化代码:

pip install yapf

编辑器我采用的是 vscode,别问我为什么不用 pycharm,我现在开发用的电脑还是大学买的 13寸超级本,性能一般,打开 pycharm 实在是慢。

正如你所想,我们公司没有开发电脑,每天盯着我这13寸的屏,感觉眼睛都要瞎了。。。

image

后端代码

代码我就不一行一行写出来解释了,直接写在代码的注释里:

# run.py

'''
    render_template 是渲染模板用的,这里我们用来返回 index.html
    flask_cors 用来解决跨域的问题
'''
from flask import Flask, render_template
from flask_cors import CORS

# 通过 static_folder 指定静态资源路径,以便 index.html 能正确访问 CSS 等静态资源
# template_folder 指定模板路径,以便 render_template 能正确渲染 index.html
APP = Flask(
    __name__, static_folder="../dist/static", template_folder="../dist")

CORS(APP)


@APP.route("/")
def home():
    '''
        当在浏览器访问网址时,通过 render_template 方法渲染 dist 文件夹中的 index.html。
        页面之间的跳转交给前端路由负责,后端不用再写大量的路由
    '''
    return render_template('index.html')


@APP.route('/test', methods=['GET', 'POST'])
def test():
    ''' 这个API用来测试跨域 '''
    return 'success'


if __name__ == '__main__':
    # 开启 debug模式,这样我们就不用每更改一次文件,就重新启动一次服务
    # 设置 host='0.0.0.0',让操作系统监听所有公网 IP
    # 也就是把自己的电脑作为服务器,可以让别人访问
    APP.run(debug=True, host='0.0.0.0')

启动后端服务:

python run.py

image

开发环境测试

写个 ajax 请求,调用服务端的 /test 接口,然后执行:

npm run dev

image

打开控制台可以看见,我们跨域调用成功了。

生产环境测试

随便写点用来测试的前端代码,然后执行:

npm run build

然后在浏览器中访问:

你的电脑 IP + 端口号

比如 http://192.168.5.18:5000,页面成功展示,静态资源也成功加载。

image

生产环境也没有问题,完美收工!

前端代码我没有细写,因为在写这篇文章的时候我已经写了一些业务代码了,所以就不给大家贴出来了,不过我相信大家的前端代码应该也没什么问题,如有疑问,可以在下面留言。

JavaScript 进阶 - 变量

这一节主要讲如何在 JavaScript 中创建变量,JavaScript 中的变量有什么特点,以及几种创建方式都有什么区别。

JavaScript 变量的特点

JavaScript 是一门弱类型语言,所以在声明变量时不需要指定变量类型,与强类型语言不同,JavaScript 会在运行过程中自动推导变量类型。

好处是书写方便,编写代码效率很高,但缺点也很明显,当类型发生错误,我们只能在代码运行时发现。

JavaScript 变量的定义

JavaScript 定义变量很简单,通常有三种方式:

// 使用关键字 var
var name = '学长';
var age = 18,
	gender = 'boy';

// 使用关键字 let
let name = '学长';
let age = 18,
	gender = 'boy';

// 使用关键字 const
const name = '学长';
const age = 18,
	gender = 'boy';

但与 var 不同的是,let、const 不可重复定义相同变量:

// 使用关键字 var
var name = '学长';
var name = 'Tony';
console.log(name); // 'Tony'

// 使用关键字 let
let name = '学长';
let name = 'Tony';
console.log(name); // Uncaught SyntaxError: Identifier 'name' has already been declared

// 使用关键字 const
const name = '学长';
const name = 'Tony';
console.log(name); // Uncaught SyntaxError: Identifier 'name' has already been declared

另外,使用 const 定义变量时,必须给变量赋值,否则会报错:

// 使用关键字 var
var name; // undefined

// 使用关键字 let
let name; // undefined

// 使用关键字 const
const name; // Uncaught SyntaxError: Missing initializer in const declaration

而且使用 const 定义的变量不可重新赋值:

const name = 'Tony';
name = '学长'; // Uncaught TypeError: Assignment to constant variable

var、let、const 三者特点

ES6 规范为何要新增 let、const 两个关键字?它们三者都各自有什么特点?

只有彻底了解它们的特性,才能更好的在工作中使用,而这也是初级前端工程师晋升中级的必备进阶知识点。

块级作用域

在 ES6 之前是没有块级作用域的,那么什么叫块级作用域呢?我们通过代码来看看:

if (true) {
	var name = 'Jerry';
}
console.log(name); // Jerry

if 语句的 {} 之间就属于一个代码块,这个块本应有它自己的作用域,但在 ES6 之前都是没有的。

因此我们在 if 语句外打印 name,也是可以拿到值的。

不过现在可以通过 let、const 来实现块级作用域了:

if (true) {
	const name = 'Jerry';
}
console.log(name); // undefined

这个时候,打印出来的 name 值就是 undefined 了,name 变量只在 if 语句块里有效。

以前面试的时候经常会问到这样一个问题,看下面代码,最终会打印什么?

var arr = [0, 1, 2, 3]
for (var i = arr.length - 1; i >= 0; i--) {
    setTimeout(() => {
        console.log(arr[i])
    }, i * 1000)
}

肯定有人认为是 0 1 2 3,每隔一秒打印一个数字。然而实际却是,每隔一秒打印一个 undefined。

以前要解决这个问题就是通过立即执行函数来解决,不过现在使用 let 也同样可以解决这个问题:

var arr = [0, 1, 2, 3]
for (let i = arr.length - 1; i >= 0; i--) {
    setTimeout(() => {
        console.log(arr[i])
    }, i * 1000)
}
// 0
// 1
// 2
// 3

变量提升

什么是变量提升?我们先来看一段代码:

function fn() {
    console.log(name);
    var name = 'tony';
}

fn();  // undefined

可以看到我们在定义变量 name 前调用 name,程序并没有报错,而是打印了 undefined。

这就是变量提升,通过 var 定义的变量会被提升到它所在作用域的顶部,因此上面的代码也可以写成:

function fn() {
    var name;
    console.log(name);
    name = 'tony';
}

fn();  // undefined

暂时性死区

如果把上面代码的 var 关键字换成 let 或者 const 会是什么情况呢?

function fn() {
    console.log(name);
    const name = 'tony';
}

fn(); // Uncaught ReferenceError: Cannot access 'name' before initialization

可以发现,在变量声明之前引用这个变量,程序将抛出引用错误(ReferenceError)。

因为这个变量将从代码块一开始的时候就处在一个暂时性死区,直到这个变量被声明为止。

全局变量

通过 var 定义的变量还有一个特点,那就是如果在全局环境中定义一个变量,程序会自动在 window 对象上面添加一个相同的属性。

var name = "学长";
window.name // '学长'

然而通过 let、const 定义变量就不会出现这种情况:

let name = '学长';
window.name // undefined

总是使用 const 和 let 定义变量

var 关键字的这两个特性使得代码容错性更高,但实际上却更容易让人犯错,再来看看下面一段代码:

var name = '学长';
function fn() {
     console.log(name);
     if (false) {
        var name = "Tony";
    }
}
fn();

大家先想想,这里打印出来的 name 值是什么?

肯定很多初学者会认为打印值是 学长,但实际这里打印的值却是 undefined

是不是感觉有点莫名奇妙,其实解释起来也很简单。

var 定义的变量是没有块级作用域的,再配合其变量提升的特性,那么上面的代码就相当于:

var name = '学长';
function fn() {
    var name;
    console.log(name);
    if (false) {
        name = "Tony";
    }
}
fn();

这样子写,是不是就能看明白了。

规范 commit 与 changelog 生成

一个良好的提交习惯,绝对会为以后的代码维护带来不小的收益。

举个例子,某天你自己写的一个功能出问题了,找到代码被修改的地方,发现是一个同事修改了一行代码,这个时候你一脸懵逼,因为他既没有写注释,commit 记录也只是随便写了一句,完全搞不明白为什么要那样修改。你想改回去,又怕影响其它的地方,不改回去,就只能想办法改自己原来的逻辑。

一个好的提交信息完全可以避免这种情况,changelog 信息也同样如此,能很好的帮助回归一段时间内都做了些什么,了解项目的整个进度,并且有助于 code review。

Git Commit 规范

规范采用 Angular Git Commit Guidelines

Commit Message 格式

每条 Commit message 都包括三个部分:header, bodyfooter

header 比较特殊,也包含三个部分:type, scopesubject

具体格式如下:

<type>(<scope>): <subject>
<BLANK LINE>
<body>
<BLANK LINE>
<footer>

每行不能超过 50 个中文长度。

Revert

If the commit reverts a previous commit, it should begin with revert:, followed by the header
of the reverted commit.
In the body it should say: This reverts commit <hash>., where the hash is the SHA of the commit
being reverted.
A commit with this format is automatically created by the [git revert][git-revert] command.

Type(必须)

本次 commit 的类型,必须是下面几项之一:

  • feat: 一个新功能
  • fix: 一个 bug 修复
  • docs: 仅仅修改了文档,比如 README, CHANGELOG, CONTRIBUTE 等
  • style: 不影响代码逻辑的修改,比如空格、格式缩进、删除分号等
  • refactor: 代码重构
  • perf: 提升性能的改动
  • test: 增加或修改测试
  • chore: 改变构建流程、或者增加辅助工具、依赖库等

Scope(可选)

scope 用于指定 commit 影响的范围

You can use * when the change affects more than a single scope.

Subject(必须)

subject 是对此次修改的简短描述:

* 以动词开头,使用第一人称现在时,比如change,而不是changed或changes
* 首字母小写
* 结尾不加句号

Body(可选)

对此次修改目的与动机的详细文字说明:

* 具体增加了什么功能?
* 为什么要这样修改?
* 如何解决这个问题的?
* 是否存在副作用或其它风险?

Footer(可选)

Breaking Changes 即破坏性变动,比如不兼容修改。

也可以用来关闭 issues,Closes #123, #245, #992。

在普通业务项目中基本用不到这个。

commit 工具 —— commitizen

首先安装 Commitizen cli:

npm install commitizen -g

然后,在你的项目里运行下面的命令(每个新的项目都需要):

commitizen init cz-conventional-changelog --save-dev --save-exact

之后,就可以用 git cz 代替 git commit 进行提交了:

image

具体文档查看 cz-cli

commit 模板

对于一般的项目开发而言,每次提交都使用 commitizen cli 还是会比较费时间,而且按快了容易选错,所以在日常开发中更建议使用 commit 模板。

方法一:

右键打开 TortoiseGit 设置界面

image

添加 commit 模板

image

注:路径应为两个反斜杠或斜杠进行分割,模板需要自己创建一个 txt 文件并编写。

使用 TortoiseGit 提交时的样子:

image

方法二:

使用原生 git

git config  — —global commit.template  [模板文件名] 

注:这个方法没用过,如果遇到问题,请自行百度

自动生成 Change log

只要 commit message 符合 Angular 那套规范,我们就可以用 standard-version 这样的工具来自动生成 CHANGELOG 文件。

首先安装到开发环境

npm install --save standard-version

然后配置 package.json

"scripts": {
    "release-f": "standard-version -f",
    "release-major": "standard-version -r major",
    "release-minor": "standard-version -r minor",
    "release-patch": "standard-version -r patch"
}

CLI 命令选项:

--release-as, -r     Specify the release type manually (like npm version <major|minor|patch>)
                                                                                        [string]
--prerelease, -p     make a pre-release with optional option value to specify a tag id [string]
--infile, -i         Read the CHANGELOG from this file                [default: "CHANGELOG.md"]
--message, -m        Commit message, replaces %s with new version
                                                        [string] [default: "chore(release): %s"]
--first-release, -f  Is this the first release?                      [boolean] [default: false]
--sign, -s           Should the git commit and tag be signed?        [boolean] [default: false]
--no-verify, -n      Bypass pre-commit or commit-msg git hooks during the commit phase
                                                                    [boolean] [default: false]
--commit-all, -a     Commit all staged changes, not just files affected by standard-version
                                                                    [boolean] [default: false]
--silent             Don't print logs and errors                     [boolean] [default: false]
--tag-prefix, -t     Set a custom prefix for the git tag to be created  [string] [default: "v"]
--scripts            Provide scripts to execute for lifecycle events (prebump, precommit,
                    etc.,)                                                       [default: {}]
--skip               Map of steps in the release process that should be skipped   [default: {}]
--dry-run            See the commands that running standard-version would run
                                                                    [boolean] [default: false]
--version, -v        Show version number                                              [boolean]
--help, -h           Show help                                                        [boolean]

major:通常代表一个大的版本更新;minor:代表功能添加;patch:代表 bug 修复
major:1.0.0 -> 2.0.0, minor: 1.0.0 -> 1.1.0, patch : 1.0.0 -> 1.0.1

参考文章:

  1. commit-message
  2. Commit message 和 Change log 编写指南

必须知道的 Promise 进阶点(一)

本文不对 Promise 的基本用法和 API 做介绍,只是对一些进阶用法做总结。

异步函数中抛出的错误,无法被 catch

很多文章里都会说 reject() 与 throw 效果一样,这种说法并不完全正确。

如果在同步操作里用 throw 抛出错误,两者效果相同,但是如果在异步函数中抛出的错误,是无法被 catch 到的。

举个例子:

new Promise(function(resolve, reject) {
    setTimeout(function() {
        throw 'Uncaught Exception!'
    }, 100)
})
.catch(function(err) {
    console.log('打印错误', err) // 不会执行
})

这段代码会抛出错误,但是 catch 函数里的代码并不会执行,换作 reject 则可以:

new Promise(function(resolve, reject) {
    setTimeout(function() {
        reject('Caught Exception!')
    }, 100)
})
.catch(function(err) {
    console.log('打印错误', err) // 打印错误 Caught Exception!
})

用 .catch() 捕获异常

.catch() 的作用与 then(resolved, rejected) 中 rejected 回调的作用几乎一致。但是 resolved 回调里抛出的错误在 rejected 是无法捕获的,所以尽量使用 .catch() 来进行错误捕获。

.then() 回调函数的返回值

在 .then() 的回调函数里通常会返回三种类型的值:

1. return 另一个 promise

var anotherPromise = function() {
    return Promise.resolve('bar')
}

Promise.resolve('foo')
    .then(function(value) {
        console.log(value) // foo
        return anotherPromise()
    })
    .then(function(result) {
        console.log(`result is: ${result}`) // result is: bar
    })

2. return 一个同步的值 (如果没有明确返回值,会默认返回 undefined)

var anotherPromise = function() {
    return Promise.resolve('bar')
}

Promise.resolve('foo')
    .then(function(value) {
        console.log(value) // foo
        anotherPromise()
        return '同步值'
    })
    .then(function(result) {
        console.log(`result is: ${result}`) // result is: 同步值
    })

Promise.resolve('foo')
    .then(function(value) {
        anotherPromise()
    })
    .then(function(result) {
        console.log(`result is: ${result}`) // result is: undefined
    })

3. throw 一个同步异常(不是返回值,但在这里把它当作返回值)

var anotherPromise = function() {
    throw 'oh error!'
}

Promise.resolve('foo')
    .then(function(value) {
        return anotherPromise()
    })
    .catch(function(err) {
        console.log(`result is: ${err}`) // result is: oh error!
    })

Promise 穿透

.then(resolved, rejected) 中的参数 resolved rejected 必须是两个回调函数。

假如传的不是函数,而是直接调用函数,那么上一个 Promise 返回的值会跳过这个 then,直接到下一个。

function sayHello() {
	console.log('hello')
}

console.log('start')

Promise.resolve('foo')
    .then(sayHello())
    .then(function(result) {
        console.log(`result is: ${result}`)
    })

console.log('end')

// 会依次打印
start
hello
end
result is: foo

参考文章

Promise进阶
We have a problem with promises
Promise - JavaScript | MDN

在 vue 组件中查看 vuex 定义

在进行 vue 项目开发的时,如果项目中用到了 vuex 做状态管理,经常需要查看 store 里面定义的状态属性。但是在 vue 组件中引用这些 vuex 属性,并非像引用静态资源那样通过路径去查找,所以不能直接跳转到定义。

针对这个痛点,我写了一个 vscode 的插件 vuex peek,效果如下:

具体效果

使用方法

我们在组件中使用 state 和 action 这些属性和方法时,完全不能体现出路径,所以要想实现跳转,就必须遵循一定的规则,才能解析。

  1. 所有定义 vuex 相关的 store 文件都要放在 store 文件夹里,如果你的项目很简单,只有一个 store.js,那这个插件对你就暂时没有用。

  2. mutations actions getters 这些都要作为一个模块单独分开,就像这样:

文件划分

  1. 在组件中声明这些 vuex 属性时,需要添加以下前缀:

    // Add `vxs` for State alias
    ...mapState({
        vxsAccountInfo: state => state.account.accountInfo
    })
    // Add `vxg` for Getters alias
    ...mapGetters({
        vxgDoneCount: 'doneCount'
    })
    // Add `vxa` for Actions alias
    ...mapActions({
        vxaGetStudent: 'student/getStudent'
    })
    // Add `vxm` for Mutations alias
    ...mapMutations({
        vxmSetStudent: 'student/setStudent'
    })
  2. 需要在 settings.json 中配置 store 文件夹在工程里的位置,因为我们的工程是多页面,每个页面都有一个对应的 store,如果不配置,默认是 src,所以如果你的项目只有一层,那就不需要配置这个:

    {
      ...
      "vuex_peek.storePath": [
        "src/entries/manager",
        "src/entries/teacher",
        "src/entries/student"
      ]
    }

我目前在自己的工程里使用了一下,暂时没什么问题,大家可以下载试试,有问题可以提 issue 或者直接提 PR,后面我也会对代码进行一些修改,尽量减少上面的限制。

相关参考

VS Code 插件开发攻略
VS Code 插件开发文档
VS Code API

【译】使用 CSS Flexbox 创建等高的价目表

原文链接 Creating an Equal Height Pricing Table using CSS Flexbox

这篇文章是我在众成翻译上的最新译文

使用 CSS Flexbox 创建等高的价目表(Pricing Table)

作者: Guest Post by Jon Muller | Feb 4th, 2018

在我看来,价目表是让潜在顾客一眼就能快速捕捉并获得你的服务和优点的最简洁有效的方法。最近,我正在为我的网站寻找一种好的价目表,然后发现几乎这些所有价目表都有一个问题——它们不是垂直响应式的。我的意思是价目表的每一列都有属于它自己基于里面内容数量的高度。我需要一种所有列高度都一样的等高价目表,不使用表格。我的解决方案?CSS Flexbox.

这里有一个我将展示给你,通过使用 CSS Flexbox 创建的等高价目表的例子。注意每一列的高度是如何与兄弟列相等的,即使每一列都有它们自己不同行数的内容。此外,用来放点击按钮的最后一个 LI 也总是底部对齐的:

例子请看:Equal Height Pricing Table using CSS Flexbox.

这里是两条创建等高价目表的关键 CSS 样式:

image

HTML 标记

让我从 HTML 标记开始,我希望能尽量干净简洁。为此,每一个价目表我都只是简单的使用了一个 UL 列表, 然后把它们用一个 DIV 容器包裹起来:

<div>
	<ul>
		<li><b>2nd Place</b><br />Herman Miler</li>
		<li><b>Weight:</b> 55 lbs</li>
		<li><b>Max Weight:</b> 330 lbs</li>
		<li><a href="#"><span></span> Check Out</a></li>
	</ul>
	<ul>
		<li><b>1st Place</b><br />Argomax Chair</li>
		<li><b>Material:</b> Nylon w/ Breathable Glass Fiber</li>
		<li><b>Head Rest:</b> Yes</li>
		"
		"
		<li><a href="#"><span></span> Check Out</a></li>
	</ul>
	<ul>
		<li><b>3rd Place</b><br />Eurotech Mesh</li>
		<li><b>Dimensions:</b> 24.8W x 47.3H</li>
		"
		<li><a href><span></span> Check Out</a></li>
	</ul>
</div>

正如你所见,每一个 UL.theplan 元素都包含了不同数量的 LI 条目。目标是让每一个 UL 的高度都一样, 并且每条价格计划的最后一条 LI 条目都排在最底部。

我找到了实现这种效果的最简方式?使用 CSS Flexbox 并且给每个 UL 设置flex-direction:column 以便它们垂直的排列来适配最长 flex 子元素的高度。下面我会更详细的解释。

The CSS

这里是等高价目表的 CSS。我移除了不重要的部分,因此你可以集中那些重要的部分:

.pricingdiv{
	display: flex;
	flex-wrap: wrap;
	justify-content: center;
}

.pricingdiv ul.theplan{
	list-style: none;
	margin: 0;
	padding: 0;
	display: flex;
	flex-direction: column;
	width: 260px; /* width of each table */
	margin-right: 20px; /* spacing between tables */
	margin-bottom: 1em;
	border: 1px solid gray;
	transition: all .5s;
}

.pricingdiv ul.theplan:hover{ /* when mouse hover over pricing table */
	transform: scale(1.05);
	transition: all .5s;
	z-index: 100;
	box-shadow: 0 0 10px gray;
}

.pricingdiv ul.theplan:last-of-type{ /* remove right margin in very last table */
	margin-right: 0;
}

/\*very last LI within each pricing UL \*/
.pricingdiv ul.theplan li:last-of-type{
	text-align: center;
	margin-top: auto; /\*align last LI (price botton li) to the very bottom of UL \*/
}  


@media only screen and (max-width: 600px) {
	.pricingdiv ul.theplan{
		border-radius: 0;
		width: 100%;
		margin-right: 0;
	}

	.pricingdiv ul.theplan:hover{
		transform: none;
		box-shadow: none;
	}
	
	.pricingdiv a.pricebutton{
		display: block;
	}
}

我通过给父 DIV 容器设置 display:flex 开始,并且允许子 flex 元素换行并水平居中,使用 flex-wrap: wrapjustify-content: center。所有子 UL 元素都是 flex 元素。

每一个由 UL 元素组成的价目表,都设置了 flex-direction:column。默认情况下,flex 子元素(flex children)都在水平轴展现。通过设置 flex-direction:column,我强制所有 flex 子元素的默认行为都作用在垂直面,包括黄金奖(golden prize)—— 默认等高的flex 子元素

每一个 UL 价目表的最后一个 LI 元素底部对齐

这样,每个在 DIV 里的价目表,现在高度都一样了。但是仍然有需要改进的地方让一切看起来更精致。我想让包含在每个 UL 的最后一个 LI 中的动作按钮与表格的最底部对齐。

我们需要两步来实现。首先,我通过 display: flex 把每个 UL 价目表也设置为 flexbox 容器。一旦这样做了,我可以使用 margin 属性来对齐一个和兄弟元素不同的特殊的子元素,比如左或者右对齐的水平 flex 子元素,或者这种情况下的垂直 flex 子元素,上面或者底部。

要让每个最后一个 LI 元素底部对齐,需要给这些元素添加点魔法原料 margin-top: auto

.pricingdiv ul.theplan li:last-of-type{
	text-align: center;
	margin-top: auto; /* align last LI (price botton li) to the very bottom of UL */
}

如果你不熟悉使用 margin 让一个 flex 子元素和其他子元素对齐的技巧,请看 this very helpful section on aligning CSS flex children elements

结论

正如你所看到的,CSS Flexbox可以创建高度相等,响应式,甚至咋页面居中的元素。它帮助我解决了我见过的许多CSS定价表的问题。

使用 electron-updater 自动更新应用

前端工程师可以使用 Electron 非常方便的编写出 PC 端应用,而应用更新的方式也有很多,详细可见更新应用程序

我的项目是基于 electron-vue 搭建的,构建打包生成安装包,则用的是 electron-builder,所以更新自然选择 electron-updater。

  1. 要实现自动更新,首先需要安装 electron-updater 包:
yarn add [email protected] -D

这里有个坑,如果你跟我一样使用的 electron-vue 搭建的项目,那 electron 的版本应该也是 2.x,截止到 2018-12-16,electron-vue 使用的依然是 "electron": "^2.0.4",如果你直接安装最新的 electron-updater,那你的更新程序是会报错的。

所以这里有两个方案,一个是升级你的 electron,还有一个是安装 3.x 的 electron-updater。

  1. 然后对 package.json 做一点小改动:
  ...
  "build": {
    "productName": "xingsanhao",
    "appId": "com.example.yourapp",
    "directories": {
      "output": "build"
    },
    "publish": [
      {
        "provider": "generic",
        "url": "http://10.0.1.42:88/"
      }
    ],
    "files": [
      "dist/electron/**/*"
    ],
    ...

build 里的内容是 electron-builder 打包需要的配置,现在我们需要在里面加入 electron-updater 需要的配置 publish, 我这里选用的是普通服务器,当然你也可以使用 GitHub 等进行免费托管,详细参考文档

  1. 创建文件服,测试环境可以直接用 express 快速搭建:
// app.js

var express = require('express')
var fs = require('fs')
const path = require('path')
var app = express()

app.use(express.static(path.join(__dirname, './client')))

var server = app.listen(88, function () {
  var host = server.address().address;
  var port = server.address().port;

  console.log('Example app listening at http://%s:%s', host, port);
});

打包生成的安装包和 latest.yml 文件就放在 client 这个文件夹里,这样我们就可以通过 http://10.0.1.42:88/latest.yml 访问到 latest.yml 这个文件。

所以在 package.json 里的配置中,publish 下的 url 项就直接写 http://10.0.1.42:88/ 就行。

  1. 最后,检查更新的代码

代码我就不写了,百度和文档里都有,这个比较简单,你想要自动检查更新还是用户手动检查更新,都看你心情。

float 常见用法与问题

float 属性绝对是众多切图仔用的最多的 CSS 属性之一,它的用法很简单,常用值就 leftrightnone 三个,但是它的特性你真的弄懂了吗?

我会在这里介绍我对 float 的认识与使用,以及使用过程中遇到的问题。

对 float 的认识

1. float 元素具有 BFC 模型特性

当给元素添加 float 属性后,元素便会具有 BFC 模型的效果。比如给内联元素 span 等添加 float 属性后,内联元素也可以设置宽高和 margin。

2. float 与 position

当给元素添加了绝对定位 absolute 或者 fixed 后,元素的浮动效果就会消失,即便 float 属性设置在 position 属性之后。

3. 脱离标准文档流

浮动元素会脱离标准文档流,会给它后面的兄弟元素造成影响,如果要清楚对兄弟元素的影响,只需要给紧邻的兄弟元素添加 clear: both 就好,但是紧邻的兄弟元素的 margin 依然是相对于父元素的。

4. 破坏父元素高度

当父元素没有设置高度,也不是 BFC 模型时,如果给子元素添加浮动效果,那么便会造成父元素高度的坍塌。

要清除浮动给父元素带来的破坏效果,方案也有很多,最直接的是把父元素变成 BFC 模型的元素就行。

不过大家使用最多的方式应该是添加一个 .clearfix 的类,不过对于这个类的写法有很多种,而我一般使用的是张鑫旭老师的方法,代码量最少:

.clearfix {
    zoom: 1;
}
.clearfix::after {
    content: '';
    display: table;
    clear: both;
}

5. float 与 padding

浮动元素不会超过父元素的内边距 padding。

利用第五点与第三点,我们在方便的实现一些布局效果并减少层级嵌套。

比如我们通常会遇到如下的样式布局:

image

代码实现:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>浮动</title>
</head>
<body>
  <div class="container">
    <p class="tt">1. 这是标题</p>
    <button class="btn">删除</button>
    <div class="con">
      这里是具体的内容
    </div>
  </div>
</body>
</html>
.container {
  width: 400px;
  padding: 20px;
  border: 1px solid #ccc;
}
.tt {
  float: left;
  margin: 0;
  width: 200px;
  overflow: hidden;
}
.btn {
  float: right;
}
.con {
  padding-top: 10px;
  clear: both;
}

标题栏既有文本也有删除等按钮,我们直接使用 float,而下面的内容部分我们通过 clear: both; 来让显示位置正确。标题栏部分和内容部分的间距则通过给 .con 元素添加 padding 而不是 margin 来控制,因为它的 margin 是相对于父容器的。

6. float 与 margin

两个相邻的浮动元素,当第一个浮动元素的宽度为100%时,第二个浮动元素会被挤到下面,通过添加负的 margin-left 或者 margin-right 值(绝对值最少等于它自身的宽度),可以使它回到第一行。

常见布局

利用这一点,我们也可以实现很多布局,比如:

在书写html代码时,我们通常的习惯根据UI样式,从左往右来写代码,但有时候右侧的内容比较重要,所以它的html结构需要放在左侧内容上面,让它更早的加载。

图一

  <div class="comment">
    <!-- 右侧重要内容 -->
    <div class="content">
      <div class="author">
        <span class="name">哇哈哈</span>
        <span class="date">2016-78-55</span>
      </div>
      <p class="text">吃的再多也不长胖,好愁人啊,怎么能快速长胖呢,在线等,急!吃的再多也不长胖,好愁人啊,怎么能快速长胖呢,在线等,急!吃的再多也不长胖,好愁人啊,怎么能快速长胖呢,在线等,急!吃的再多也不长胖,好愁人啊,怎么能快速长胖呢,在线等,急!吃的再多也不长胖,好愁人啊,怎么能快速长胖呢,在线等,急!吃的再多也不长胖,好愁人啊,怎么能快速长胖呢,在线等,急!吃的再多也不长胖,好愁人啊,怎么能快速长胖呢,在线等,急!</p>
      <div class="meta">
        <span class="msg-tag"></span>
        <span class="msg-tag">回复</span>
      </div>
    </div>
    <!-- 左侧内容 -->
    <a href="#" class="avatar"><img src="images/header.jpg" alt="头像"></a>
  </div>
* {margin:0; padding:0;}
li {list-style: none;}
a {text-decoration: none;}
body {font-family: '微软雅黑';}

.wrap {
  width: 800px;
  margin: 50px auto;
}
.content {
  float: right;
  margin-left: 100px;
}
.date {
  font-size: 14px;
  color: #666;
}
.text {
  margin: 20px 0;
}
.avatar {
  float: left;
  margin-right: -80px;
}
.avatar img {
  width: 80px;
  height: 80px;
  border-radius: 50%;
}

如上面图的效果,尽管在UI上,.content 元素在 .avatar 右边,但我们在 html 结构中,仍然需要把 .content 元素放到 .avatar 元素前面,这个时候就可以通过给 .content 元素设置为右浮动,然后给 .avatar
元素设置左浮动,再添加负 margin-right 值,让它回到上面。

2. 右侧定宽流式布局

image

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>test</title>
</head>
<body>
  <div class="content">
    <div class="box1">
      <div class="inner"></div>
    </div>
    <div class="box2"></div>
  </div>
</body>
</html>
.content {
  width: 500px;
  overflow: hidden;
}
.box1 {
  box-sizing: border-box;
  float: left;
  width: 100%;
  height: 50px;
  padding-right: 220px;
  border: 1px solid #ccc;
}
.inner {
  width: 100%;
  height: 40px;
  border: 1px solid #f23;
}
.box2 {
  float: right;
  width: 200px;
  height: 30px;
  margin-left: -100%;
  border: 1px solid #2fe;
}

前端构建工具吐槽与parcel极简入门

前端自动化构建工具,我们都经历了什么

在grunt+bower大行其道的时候,我还是一个刚进前端的fresh bird,记得那时刚在慕课网上看了一个grunt教学视频,还没来得及体验就发现大家已经开始用gulp了。

然后我屁颠屁颠的跑去找gulp的教程,跟着写了个demo。当我在命令行中敲下gulp的那一刻,我的内心是相当激动的。

image

这跟grunt比也太尼玛简单了!!

然鹅。。。好景并不长。。。

在使用gulp不到半年的时间,webpack横空出世,请原谅我直接忽略了Browserify,这玩意儿实在是有些丑陋,在webpack出来之前我一直用sea.js作为我的模块化工具。

不过在webpack1.x的版本中,我更多使用的是gulp-webpack,原因无他,webpack基于配置的格式让人看上去就像grunt一样让人不爽。

在webpack快升到2.X的时候,rollup带着three shaking来了,不过由于Vue的原因,对于rollup我只是浅尝辄止,不过这家伙的确比webpack简单不少。

在写Vue的时候一直使用vue-cli,有时候需要改改配置,所以不得不看看webpack2.X的文档,或许是受了rollup的刺激,webpack在我还没把2.X的文档看完的时候,3.X出来了。

那一刻,我的内心是奔溃的。。。

image

老子不看了!!!

就这样,传统的如 .jsp 那样的页面,我依旧使用gulp,而SPA模式的页面,当然是厚颜无耻的用着vue-cli,不对开源界做出一点贡献。

最后差点漏掉了一个家伙——FIS,现在最新的版本是FIS3,不过我记得2015年就已经是FIS3了。。。

FIS也是一个大而全的构建工具,当初学gulp的时候简单体验了一下,感觉说不上来,或许是我那时的能力还不足以驾驭它,反正配置起来也挺麻烦,基本上也就百度自家用,所以不多说了。

来自parcel的拯救

我自认为也算是一个相当能,并且愿意折腾的人了,但也着实被这些层出不穷的轮子搞得有点受不了。甚至有那么一瞬间,我感觉自己的铁杵都要被磨成针了。。。

image

对于Vue而言,有vue-cli这个强大而又方便的脚手架,然后多页面下,gulp却已经无法满足欲望日渐强盛的我了。

image

不过我一直相信一句话,车到山前必有路,船到桥头自然直。终于就在昨天,2017年12月10日下午12点的样子,我发现了一篇由奇舞团某美媛(其实我并不认识)写的文章一个比webpack快10倍的打包工具(订阅号上的,没找到文章地址)。

对于新东西,我一向是很乐意去尝试的,虽然可能更多的也仅仅是停留在尝试的阶段,就像rollup和react这些东西一样,当然他们两个都不新了。

此媛的文章写的有点长,不过我还是耐心看完了,但是我一直看到结尾后都没明白这个东西怎么用。。。

WTF!!!!

写这么多,连个demo都不给个!!

于是我开始百度,发现百度没有parcel这个东西(不过今天能搜到几篇文章了),上掘金一看,发现这果然是个新家伙,接着去github找到了官网。

把官网的文档看完后,我的内心是有些凌乱的。。。

image

这家伙到底怎么用??

然后我再次回到了Getting Started,小心翼翼的敲下那不足10行的代码,然后按下回车不到1秒的时间。

卧槽!!! It just work!!!

这尼玛,果然是零配置!!果然是快又小!!

抑制住内心的激动,我又着手开始写一个正经点的demo,既然是模块打包工具,肯定得有模块才行嘛。

好吧,这里才是正文

准备工作

安装parcel,yarn global add parcel-bundler

创建一个 package.json 文件,yarn init -y

示例的目录结构和代码

image

<!-- index.html -->

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>test</title>
	<link rel="stylesheet" href="./css/index.css">
</head>
<body>
	<p id="txt">点击给大佬戴帽:</p>
	<div id="btnBox"></div>
	<div class="imgBox">
		<img class="hat" src="./img/hat.png" alt="">
		<img src="./img/capping.jpg" alt="">
	</div>

	<script src="./js/index.js"></script>
</body>
</html>
/* index.css */

.imgBox {
	position: relative;
}
.hat {
	background-color: #fff;
    position: absolute;
    top: 126px;
    left: 310px;
    width: 143px;
    transform: rotate(-6deg);
}
// index.js

const setButton = require('./setButton').setButton;
const setColor = require('./setColor').setColor;

const btnsTxt = ['原谅的颜色', '寂寞的颜色', '花儿的颜色'];
document.getElementById('btnBox').innerHTML = setButton(btnsTxt);

const btns = document.getElementsByClassName('btn');
for(let i = 0; i < btns.length; i++) {
	btns[i].onclick = function () {
		const color = setColor(this.textContent)
		document.getElementsByClassName('hat')[0].style.backgroundColor = color
		document.getElementById('txt').style.color = color
	}
}
// setButton.js

function setButton(arr) {
	let btnHtml = '';
	for(let i of arr) {
		btnHtml += `
			<button class="btn" type="button">${i}</button>
		`
	}
	return btnHtml;
}

module.exports = {
	setButton: setButton
}
//setColor.js

function setColor(tip) {
	let color;
	switch (tip) {
		case '原谅的颜色':
			color = '#00db00'
			break;
		case '寂寞的颜色':
			color = '#ffff37'
			break;
		case '花儿的颜色':
			color = '#f75000'
			break;
		default:
			color = '#000'
			break;
	}
	return color;
}

module.exports = {
	setColor: setColor
}

运行parcel

命令行中输入,parcel index.html

image

效果图

image

完美运行!!

遇到的坑

一个新的东西出来,不管它有多好,也会存在很多坑的,虽然这个demo很简单,但是也遇到了几个问题,这里简单列举一下:

修改html文件,页面不自动更新

这个不是什么bug,官网很明确的写了,热更新只针对cssJavaScript,但是在实际开发过程中,对于html文件的监听也是必须的。

修改css文件,页面不自动更新

这个就是个问题了,在一个比webpack快10倍的打包工具这篇文章里也有提到,需要使用PostCSS,这个得后面试试。

更换图片,内容不更新

换图片不换图片名字的情况下,手动刷新页面,页面中的图片也不会更换

MOZ_TO_ME[node.type] is not a function

就上面的实例代码,执行parcel index.html不会有任何问题,但如果直接parcel build index.html就会报错:
err

在issues中找到个一样的问题UnhandledPromiseRejectionWarning when building

在回答里说跟Windows有关,不过我试了下,并不是Windows的问题,然后照着他的答案,加了后缀parcel build index.html --no-minify

加完就好用了,所以应该是uglify-js的原因,上网搜了下,uglify-js的确不支持直接压缩包含ES6语法的代码。然后我又安装了babel-preset-env,发现并没有像文档里说的那样直接就能编译,不知道是哪里有问题,只能后面再试试,先把手里的工作完成。

结语

总体而言,对于parcel的初步尝试还是很满意的,就现在看来,如果就是单纯的压缩打包,并且JavaScript用的是ES6以下的语法,的确能做到零配置。后面还得加上PostCss和Babel再看看,但是按照官网的介绍也都很简单,全都集成好的,只是用法上还需要看看。

在掘金上看到有朋友说使用parcel打包后的体积比webpack大,这个没有测试过,但是我们的项目多是PC端的,也不是很大,所以哪怕差个几十KB都不是什么问题,但是速度真的是超级快啊!

另外文档也是简单的不要不要的,以至于连个完整的demo都没有,这周我会做个简单但是尽量详细的demo出来,方便跟大家交流学习。

另外对于阅读英文文档有困难的朋友,我这里把文档翻译了一份parcel-doc,不过还没完成。后期也会一直跟着官网更新,有需要的可以关注一下。

【译】提交到不同URL的表单按钮

原文链接 Separate Form Submit Buttons That Go To Different URLs | CSS-Tricks

这篇文章是我在众成翻译上的译文

这是几天前想到的,我忘了在哪,但是我把它记在了我的小笔记本上,打算发到博客里。我把它写下来是因为我听到一些把它过于复杂化的东西。

听说你有一个像下面这样的表单:

<form action="/submit">
  <input type="submit" value="Submit">
</form>

当你提交表单,它会跳转到/submit。 然后你需要 另一个 提交按钮,跳转到不同的URL。为什么需要这样做不重要,任何事都有原因,毕竟网页包含太多东西。

我找到了一些人们尝试处理这个问题的其它方法。

其中一种方法是放弃提交到不同的URL,但是给每个提交按钮一个相同的name,不同的value,然后当需要处理不同问题时检查value值。

<input type="submit" name="action" value="Value-1">
<input type="submit" name="action" value="Value-2">

你可以在处理时读取value值,并且如果你想,还可以进行重定向。对于所述问题,这是一种解决办法。

另一种方法是在按钮点击时,通过JavaScript改变form的行为。有好几种方法实现,但是都归结为:

<form name="form">
  <input type="submit" onclick="javascript: form.action='/submit';">
  <input type="submit" onclick="javascript: form.action='/submit-2';">
</form>

它依赖于JavaScript工作,也不是很麻烦,但是它对渐进式增强的友好性,也的确不像它能做到的那样好。

正确的答案HTML已经为你想到了。我猜它或许并没有像它应该的那样众所周知,因此才有了这篇文章。

它是formaction属性,你可以直接放在提交按钮里,它会覆盖表单自己的action

<form action="/submit">
  <input type="submit" value="Submit">
  <input type="submit" value="Go Elsewhere" formaction="/elsewhere">
</form>

就这些。

填坑——解决微信网页中刷新的问题

前段时间在开发的时候遇到一个场景,需要在Ajax请求成功后,根据返回值修改 URL 查询参数并刷新页面。

第一次尝试

我第一次是使用的 window.location.replace() 方法,直接替换掉 URL,但是在微信端并没有像在浏览器里调试那样,直接刷新页面。

第二次尝试

然后尝试直接给 window.location.href 进行赋值,但结果是 URL 变了,却并没有刷新。

第三次尝试

随后我又尝试修改 URL 后,再执行 window.location.reload() 方法,这种方式在浏览器调试也没有任何问题,但是在微信端依旧没有任何反应。

百度找到的解决方案与问题

通过百度找到的文章都是讲微信浏览器的问题,认为在微信浏览器里 window.location.replace()window.location.reload() 方法无效。

而他们的解决方案都清一色的通过 window.location.href = XXX 进行刷新,不过需要对 URL 进行修改,例如添加一个查询参数,类似将 www.github.com 改为 www.github.com?time=218978234

让我感到郁闷的是,这种方式跟我第二种做法根本就是一样的,我也是修改的 URL 上的查询参数。起初我以为是我只是修改查询参数,而不是添加查询参数,所以造成刷新失败,但是通过添加参数的方法依旧无效。

最终答案

后来通过检查 URL,才发现修改查询参数的位置有问题,因为我们使用的是 Vue,前端路由是通过 Vue-router 进行管理的,并且采用的是 hash 模式,所以 URL 看上去是这样的:https://github.com/Mcbai/Blog/#/issues?id=14,而我修改或者添加的查询参数都是直接修改 id 或者在 id 之后添加。

这样的添加和修改对 Vue-router 是可用的,因为 hash 模式下,Vue-router 只关心 URL 的 hash 部分,也就是 # 后的信息,但是对于浏览器而言,不管是其它浏览器还是微信浏览器,这种修改并不算作是 URL 变化,所以一切刷新的方法都是无效的。

找到了根本原因所在,那解决起来就简单了。原理不变,还是修改 URL 的查询参数:

// 最开始的URL https://github.com/Mcbai/Blog#/issues?id=14
// 接受Ajax返回的参数后对URL进行修改 https://github.com/Mcbai/Blog/#/issues?id=28
// hash 变化对浏览器而言并不是有效的 URL 改变,所以通过 `window.location.href` 赋值不会刷新页面
// 而在微信浏览器中,这种情况通过 `window.location.replace()` 和 `window.location.reload()` 刷新页面都无效
// 所以做法是改变 URL 的真正查询参数
window.location.search = `?v=${Date.now()}`
// 改变后的 URL 就变成了 https://github.com/Mcbai/Blog?v=1528982584690#/issues?id=14
// 浏览器也进行了自动刷新

所以微信刷新并不完全是其他文章里说的那样,一些方法在微信浏览器中失效,只能通过 window.location.href = xxxx?v=123 的方式。

Vue表单类的父子组件数据传递

使用Vue.js进行项目开发,那必然会使用基于组件的开发方式,这种方式的确给开发和维护带来的一定的便利性,但如果涉及到组件之间的数据与状态传递交互,就是一件麻烦事了,特别是面对有一大堆表单的页面。

在这里记录一下我平时常用的处理方式,这篇文章主要记录父子组件间的数据传递,非父子组件主要通过Vuex处理,这篇文章暂时不作说明。

与文档里给的方案一样,父组件向子组件传递数据主要通过 props,子组件向父组件传递数据主要通过触发器 $emit(),只是在用法上会有些不同。

1、传递基本类型数据

当子组件内容较少时,会直接传递基本类型数据,通常为String, Number, Boolean三种。

先看个例子:

<!-- 父组件 parent.vue  -->

<template>
	<div class="parent">
		<h3>问卷调查</h3>
		<child v-model="form.name"></child>
		<div class="">
			<p>姓名:{{form.name}}</p>
		</div>
	</div>
</template>

<script>
	import child from './child.vue'

	export default {
		components: {
			child
		}
		data () {
			return {
				form: {
					name: '请输入姓名'
				}
			}
		}
	}
</script>
<!-- 子组件 child.vue  -->

<template>
	<div class="child">
		<label>
			姓名:<input type="text" :value="currentValue" @input="changeName">
		</label>
	</div>
</template>

<script>
	export default {
		props: {
			// 这个 prop 属性必须是 valule,因为 v-model 展开所 v-bind 的就是 value
			value: ''
		},
		methods: {
			changeName (e) {
				// 给input元素的 input 事件绑定一个方法 changeName 
				// 每次执行这个方法的时候都会触发自定义事件 input,并且把输入框的值传递进去。
				this.$emit('input', e.target.value)
			}
		}
	}
</script>

给子组件的 input 事件绑定一个方法 changeName ,每次执行这个方法的时候都会触发自定义事件 input,并且把输入框的值传递进去。

父组件通过 v-model 指令绑定一个值,来接收子组件传递过来的数据。但这样只是父组件响应子组件的数据,如果还要子组件响应父组件传递的数据,就需要给子组件定义一个props属性 value,并且这个属性必须是 value,不能写个其它单词。

v-model 其实就是一个语法糖,详情可以参考使用自定义事件的表单输入组件

2、传递引用类型数据

当子组件里的内容比较多时,比如子组件有多个表单元素,如果还像上面那样给每个表单元素绑定值,那就要写很多重复代码了。所以这个时候通常传递的是一个对象,传值的基本原理不变,不过写法上会有些不同。

还是先看代码:

<!-- 父组件 parent.vue  -->

<template>
	<div class="parent">
		<h3>问卷调查</h3>
		<child :formData.sync="form"></child>
		<div class="">
			<p>姓名:{{form.name}}</p>
		</div>
	</div>
</template>

<script>
	import child from './child.vue'

	export default {
		components: {
			child
		},
		data () {
			return {
				form: {
					name: '请输入姓名',
					age: '21'
				}
			}
		}
	}
</script>
<!-- 子组件 child.vue  -->

<template>
	<div class="child">
		<label>
			姓名:<input type="text" v-model="form.name">
		</label>
		<label>
			年龄:<input type="text" v-model="form.age">
		</label>
		<label>
			地点:<input type="text" v-model="form.address">
		</label>
	</div>
</template>

<script>
	export default {
		data () {
			return {
				form: {
					name: '',
					age: '',
					address: ''
				}
			}
		},
		props: {
			// 这个 prop 属性接收父组件传递进来的值
			formData: Object
		},
		watch: {
			// 因为不能直接修改 props 里的属性,所以不能直接把 formData 通过v-model进行绑定
			// 在这里我们需要监听 formData,当它发生变化时,立即将值赋给 data 里的 form
			formData: {
				immediate: true,
				handler (val) {
					this.form = val
				}
			}
		},
		mounted () {
			// props 是单向数据流,通过触发 update 事件绑定 formData,
			// 将 data 里的 form 指向父组件通过 formData 绑定的那个对象
			// 父组件在绑定 formData 的时候,需要加上 .sync
			this.$emit('update:formData', this.form)
		}
	}
</script>

props 是单向数据流,当我们需要对 props 内的属性进行双向绑定时,就需要用到 .sync 修饰符,详情请参考.sync 修饰符,这里不做赘述。

这里需要注意的是,vue 中是不能直接修改 props 的,所以如果我们要向父组件传值,还是需要通过修改 data 里的值,prop 只是作为父子之间通话的中间人存在。

另外,如果我们想要预览父组件最开始传的数据,就需要通过 watch 监听 prop 的变化,在子组件初始化的时候就把值传进去。

注意: 我在子组件里把 this.$emit('update:formData', this.form) 放在 mounted 当中的,其原因是为了避免在每个 input 标签的 input 事件中触发自定义事件,但这样写的前提是,父子组件都要共用一个对象

这也就是上面代码中,父组件中使用 :formData.sync="form" 绑定值时,form 是一个对象,而子组件中的触发自定义事件 this.$emit('update:formData', this.form)this.form 也得是一个对象。

这里还需要注意的是,如果有多个子组件使用一个对象,那就要避免这种写法,因为一个组件修改了这个对象的数据,那么其它子组件也就都跟着改变了。

所以我在用的时候都是给每个子组件分配了一个自己的对象,比如:

data () {
  return {
    parentObject: {
      child_1_obj: {},
      child_2_obj: {},
    }
  }
}

这是在父组件里定义的数据,当然名字不会这样取了。

结尾

也没什么说的了,对 Vue 还是处于使用的阶段,对其底层的东西了解还不够,我也好想读读源码,但总只是想想.....大家觉得有什么不妥的地方尽管说,大家相互交流交流。

必须知道的 Promise 进阶点(二)

async/await 基础

比起回调函数那看不懂的嵌套,Promise 要清爽不少,但当任务较多时, Promise 也有可能会有比较长的链和嵌套,这时候使用 async/await 就会让代码易读很多。

async

async 函数可以看作是 Promise 的语法糖:

// resolve 状态
async function foo() {
    console.log('start')
    return 'resolve'
}
foo().then(data => {
    console.log('data:', data)
})
console.log('end')
// start
// end
// data: resolve


// reject状态
async function foo() {
    console.log('start')
    throw new Error('reject')
}
foo().catch(data => {
    console.log('data:', data.message)
})
console.log('end')
// start
// end
// data: reject

与下面直接使用 Promise 方式书写的效果一模一样:

// resolve 状态
function foo() {
    return new Promise((resolve, reject) => {
        console.log('start')
        resolve('resolve')
    })
}
foo().then(data => {
    console.log('data:', data)
})
console.log('end')
// start
// end
// data: resolve


// reject状态
function foo() {
    return new Promise((resolve, reject) => {
        console.log('start')
	reject('reject')
    })
}
foo().catch(data => {
    console.log('data:', data)
})
console.log('end')
// start
// end
// data: reject

await

await 与其字面意思一样——等待,等待其后的函数执行完毕。

// resolve状态
async function foo() {
    console.log('start')
    return 'resolve'
}
async function bar() {
    const data = await foo()
    console.log('data', data)
}
bar()
console.log('end')
// start
// end
// data resolve

// reject状态
async function foo() {
    console.log('start')
    throw new Error('reject')
}
async function bar() {
    try {
        const data = await foo()
        console.log('data', data)
    } catch (err) {
	console.log('data', err.message)
    }
}
bar()
console.log('end')
// start
// end
// data reject

await 只能在 async 函数里使用,否则会报错

循环中的 async/await

首先思考下面两个场景:

  1. 有一个异步请求,需要多次并且按顺序发送

  2. 有一个异步请求,需要多次发送,但不用按顺序

场景一

同一个请求,多次,按顺序。这就是一个典型的串行处理

function mockServer(i) {
    return new Promise((reslove, rejecy)=> {
        setTimeout(() => {
	    reslove('有值了:' + i)
        }, 1000 * i)
    })
}

async function getData(time) {
    var data = await mockServer(time)
    return data
}

var arr = [1,2,3,4]

async function showData() {
  console.time('showData')

  for (const item of arr) {
    const data = await getData(item)
    console.log(data)
  }
  console.timeEnd('showData')
}
showData()

// 有值了: 1
// 有值了: 2
// 有值了: 3
// 有值了: 4
// howData: 13100.510009765625ms

我们通过 for-of 循环调用了 4 次异步函数 getData,由于 getData 前面加了关键字 await,所以会依次排队处理,一共花了13秒多的时间。

场景二

同一个请求,多次,不按顺序。这就是一个典型的并行处理,每个请求同时发送,而不用排队等候,节约时间。

function mockServer(i) {
    return new Promise((reslove, rejecy)=> {
        setTimeout(() => {
	    reslove('有值了:', i)
        }, 1000 * i)
    })
}

async function getData(time) {
    var data = await mockServer(time)
    return data
}

var arr = [1,2,3,4]

async function showData() {
  console.time('showData')

  var allAsyncRequest = arr.map(item => getData(item))
  for await  (const asyncRequest of allAsyncRequest) {
    const data = asyncRequest
    console.log(data)
  }
  console.timeEnd('showData')
}
showData()

// 有值了: 1
// 有值了: 2
// 有值了: 3
// 有值了: 4
// showData: 4131.318115234375ms

我们在 map 的回调里调用了 4 次异步请求函数,将请求事件放到事件队列里面,让 4 个请求可以同时处理,而不影响后续任务的执行。

然后再通过 for await...of 来等待 4 个异步请求都执行完,一共花了 4 秒,大大节约了时间。

这里的 for await...of 还可以换一种写法:

for (const asyncRequest of allAsyncRequest) {
    const data = await asyncRequest
    console.log(data)
}

也可以使用 Promise.all()

Promise.all(allAsyncRequest).then((data) => {
    console.log(data)
    console.timeEnd('showData')
})

// ["有值了:1", "有值了:2", "有值了:3", "有值了:4"]
// showData: 4441.679931640625ms

注意:这里不能使用 forEach 来进行循环处理,具体原因可以看 当async/await遇上forEach,这篇文章已经写的很清楚了。

最后

学会熟练使用 async/await,可以很好提升代码的可阅读和可维护性,大家如果还有更好的用法和建议,欢迎在评论区补充。

基于七牛和 element-ui 的 vue 文件上传组件

将图片之类的文件资源存在七牛需要使用七牛的 JS SDK,项目基于 Vue2.x,使用的 Element-UI,所以希望能直接使用 ElementUi 自带的上传组件,而不是再基于七牛的 SDK 完全重新封装一个。

七牛的文档写的真的不怎么样,虽然实际要写的代码很简单,但你直接看文档,看完一遍都不知道他写的什么。

Element-UI 的上传组件支持覆盖默认的上传行为,可以自定义上传的实现,但是 httpRequest 这个函数所接受的参数并没有在文档里写明,需要自己去看源码。

这里统一写一个简单的组件示例,大家结合这个组件,再去看七牛文档,就能很轻松理解了,只需要根据自己的需求往组件里加东西就行。

<template>
    <div class="c-upload-root">
        <el-upload
            action=""
            :multiple="true"
            :http-request="uploadFile"
            v-bind="$attrs">

            <slot></slot>

            <div class="el-upload__tip" slot="tip">
                <slot name="tip"></slot>
            </div>

        </el-upload>
    </div>
</template>

<script>
/**
 * 在其它地方调用该组件时,
 * 可以直接使用 el-upload 组件所提供的所有属性和方法,
 * 只有 action 和 http-request 两个属性无法修改
 */
import * as qiniu from 'qiniu-js'

export default {
    name: 'qn-ele-upload',
    inheritAttrs: false,
    data() {
        return {

        }
    },
    props: {
        // 上传凭证
        // 七牛JavaScript SDK API: qiniu.upload(file: blob, key: string, token: string, putExtra: object, config: object) 里的 token
        // 具体参数查看 https://developer.qiniu.com/kodo/manual/1208/upload-token
        qnToken: {
            type: String,
            default: null
        },
        // 七牛JavaScript SDK API: qiniu.upload(file: blob, key: string, token: string, putExtra: object, config: object) 里的 config
        // 具体参数查看 https://developer.qiniu.com/kodo/sdk/1283/javascript#3
        qnConfig: {
            type: Object,
            default() {
                return {
                    useCdnDomain: true,
                    disableStatisticsReport: false,
                    retryCount: 6,
                    region: qiniu.region.z2
                }
            }
        },
        // 七牛JavaScript SDK API: qiniu.upload(file: blob, key: string, token: string, putExtra: object, config: object) 里的 putExtra
        // 具体参数查看 https://developer.qiniu.com/kodo/sdk/1283/javascript#3
        qnPutextra: {
            type: Object,
            default() {
                return {
                    fname: '',
                    params: {},
                    mimeType: null
                }
            }
        }
    },
    methods: {
        /**
         * 文件上传方法,使用 七牛SDK 进行上传,覆盖 el-upload 的默认上传方法
         * @param {Object} option - 包含下列属性:
         * {
         *      headers: 使用 el-upload 组件提供的 headers 属性
         *      withCredentials: 使用 el-upload 组件提供的 headers 属性
         *      file: 添加到浏览器的 file 对象
         *      data: 使用 el-upload 组件提供的 data 属性
         *      filename: 使用 el-upload 组件提供的 name 属性
         *      action: 使用 el-upload 组件提供的 action 属性
         *      onProgress: 使用 el-upload 组件提供的 onProgress 属性
         *      onSuccess: 使用 el-upload 组件提供的 onSuccess 属性
         *      onError: 使用 el-upload 组件提供的 onError 属性
         *  }
         */
        uploadFile(option) {
            const fileName = this.changeFileName(option.file.name)
            
            const observable = qiniu.upload(
                option.file,
                fileName,
                this.qnToken,
                this.qnPutextra,
                this.qnConfig
            )
            observable.subscribe({
                next: option.onProgress,
                error: option.onError,
                complete: option.onSuccess
            })
        },
        // 修改原文件名,给文件名添加一个时间戳
        changeFileName(filename) {
            return filename.replace(/.[a-zA-Z0-9]+$/, (match) => {
                return `-${Date.now()}${match}`
            })
        }
    }
}
</script>

这里有个小技巧,就是 inheritAttrs: false 结合 $attrs 的使用,以此来保证我们基于 Element-UI 再次封装的组件可以直接使用 Element 组件提供的属性和方法,而不需要每个都通过 props 属性或者 $emit() 再写一次。

从输入网址按下回车到页面显示,都发生了写什么?

当一个用户打开浏览器,从在地址栏输入网址并按下回车键,到页面显示在界面的这短短几秒内,其背后都发上了些什么?

理解这些东西可能并不能帮你提升编码水平,但是对于一个web开发者而言,这应该是必备知识,并且你也能从中长期受益。

名词解释

  • 通信协议:人与人之间通过语言进行交流传递信息,而计算机之间的交流与数据传递则是按照某种特定的规则和约定,这种约定就是计算机之间的沟通语言,也就是通信协议,它规定了彼此之间该怎样交流,交流什么,何时交流。

    通信协议有很多种,而我们经常听到的TCP/IP协议,便是其中最重要的两种。

  • IP协议:互联网协议(Internet Protocol),属于第三层网络协议,完成路由寻址和数据传递功能,它将数据分割成一个个包,每个包中还带有源IP地址、端口和目标IP地址、端口。数据包不能保证一定送达,也不能保证送达顺序

  • UDP协议:建立在IP协议之上,通信不需要连接,只需要知道目标IP地址和端口,简单,速度快,但是容易丢包。

  • TCP协议:建立在IP协议之上,通信需要握手连接,会给IP数据包编号,以字节流的方式传输,确保到达顺序,丢包会重新发送。

  • 端口:一台计算机上可能会运行着多个程序,这个时候就需要通过端口来区分,就像一层楼会有多个房间,每个房间也有自己的房间号,这样就能避免小姐姐进错房间。

  • HTTP协议:基于TCP的客户端与服务器通信协议,可中断(请求可中断,比如取消下载),无状态(对于服务器而言,无法知道多个http请求是否来自同一个客户端),无连接(每次连接只处理一个请求,完事就断开)。

网址结构

我们通常所说的网址就是URL,它的结构如下:

scheme://host.domain:port/path/filename?querystring#anchor
  • scheme:协议名,通常为http或者https

  • host:主机名(默认是www),还包括常见的email等

  • domain:域名,而域名又有顶级域名和二级域名,多级域名用.隔开,比如github.com,.com就是顶级域名,github为二级域名

  • port:主机上的端口号(http的默认端口号是80)

  • path:文件在服务器上的路径

  • filename:文件名称

  • querystring:常用于GIT请求的查询条件,常见的类似id=123

  • anchor:锚

比如https://www.github.com/Mcbai/Blog/blob/master/README.md这个网址,它的每个部分就分别是......

你自己猜吧!

角色扮演

可以用从家到商场买东西,来比喻从输入网址到页面显示这一过程,虽然不是完全准确,但也能帮助初学者更容易理解。

  • 网络连接: 允许你在互联网上发送和接受数据。就像你要从家到商场,首先路得是通的。

  • TCP/IP:上面介绍过的协议。类似于你去商场时使用的交通工具。

  • DNS:域名系统,域名与服务器IP相互映射的一个分布式数据库。就像是商场里的引导员,你告诉她你要的商品,她告诉你卖这个商品的具体商店。

  • 后端程序:负责数据的增删改查和对前台的接口提供。类似于商店里的售货员。

  • HTTP:客服端与服务器之间的超文本传输协议。等同你与售货员之间的沟通语言,你告诉售货员你要买东西类似于http请求,售货员给你东西就类似于http响应。

详细流程

那么这一系列过程中,又具体发生了些什么?

  • 浏览器在域名系统服务器上找出存放网页的服务器的实际IP地址(走到商场,询问引导员,找到商店位置)

  • 浏览器发送HTTP请求信息到文件服务器来请拷贝一份网页到客户端(你走到商店并告知售货员你要买的东西)

  • 服务器同意客户端的请求后,会返回一个“200 OK”信息,意味着“你可以查看这个网页,给你~”,然后开始将网页的文件以数据包的形式传输到浏览器(售货员处理好你的需求并给你商品,你将商品带回家)

  • 浏览器将数据包聚集成完整的网页并解析,最终将网页呈现给你(拿到商品回家,打开包装盒,展示物品)

我们以访问www.github.com为例,主要用图说明,懒得用鼠标一会儿点一会儿拖,直接用笔画了。

第一步:DNS查询

真正的网址看上去并不像你输入的那样美好、容易记忆。它们是一串数字,像 63.245.217.105。

这串数字就叫做 IP 地址,它代表了文件服务器所在的地址。然而,它并不容易记忆,不是吗?那就是域名系统被发明的原因,它们是将你输入的网址与实际 IP 地址相匹配的特殊的服务器。

浏览器与DNS服务器交互:

浏览器缓存 -> 系统缓存 -> 路由器缓存 -> ISP 的 DNS 缓存 -> 递归搜索

dns

DNS工作原理:

dns1

当然,浏览器与DNS服务器交互,查询域名对应IP的实际过程比这要复杂好多,这只是大致的原理过程,想知道更详细的过程,动手百度吧。

第二步:连接web服务器,获取相应文件资源

获取到web服务器的IP地址后,浏览器就会通过TCP/IP协议,与服务器握手连接。然后向服务器发送http请求,并带上各种头信息。

image

服务器从http请求中解析出要访问的主机名和需要的文件资源等信息,然后交由对应的后台程序处理,成功后通过http返回数据。

image

image

完事之后断开此次http连接,大致过程如下:

wf

如果文件中包含多个资源,就会多次向服务器发送http请求。

image

结尾

以上就是浏览器客户端与服务器交互的基本原理了,理解了这些基本原理,才能更好的研究前后端交互以及相关性能优化手段。

虽然可能过程并不十分详尽,但无论TCP/IP协议还是HTTP协议和DNS,随便一样都可以拿本书来讲,所以大家可以百度查询更详细的原理,包括HTTP 2.0和DNS安全等。

参考文章

Web工作方式
网络是如何工作的
从输入网址到显示网页的全过程分析

CSSOM View Module 常用属性和方法记录

对于页面元素宽高这块的内容,单纯在 CSS 里还是比较简单的,但是在 JavaScript 里就稍显复杂了,涉及到的方法也很多,而且不同浏览器之间也存在差异,当然,主要还是 IE。

这一块的东西在《JavaScript高级程序设计》中说的也不够完整,不过 CSSOM View Module W3C 工作草案有较为详细的说明,我把其中的一些常用的属性和方法逐一进行测试,在这里做一份记录。

注意: 以下所有的属性都是在 IE9 及以上的浏览器中测试,需要兼容 IE6 7 8 的同学请自行测试!

HTMLElement 对象方法

HTMLElement 对象包含的与元素大小位置相关的方法主要有:

1. getClientRects()
2. getBoundingClientRect()

2. getBoundingClientRect()

返回一组该元素相关的只读属性——相对于浏览器视口左上角的 left、top、right和bottom,以及元素自身的 width 和 height 属性。

当元素发生滚动时,left、top、right和bottom 也会发生改变。

image

HTMLElement 对象属性

HTMLElement 对象包含的与元素大小位置相关的属性主要有:

1. clientTop / clientLeft
2. clientWidth / clientHeight
3. scrollTop / scrollLeft
4. scrollWidth / scrollHeight
5. offsetParent
6. offsetTop / offsetLeft
7. offsetWidth / offsetHeight

1. clientTop / clientLeft (只读)

获取元素的 border-left-width 和 border-top-width 的值。

2. clientWidth / clientHeight(只读)

获取元素视口的宽高,包括 padding 大小,但是不包括边框和滚动条。

image

Tip: 可以使用 document.documentElement.clientWidth / clientHeight 获取当前浏览器视口宽高。

3. scrollTop / scrollLeft (可读可写)

存在滚动条时,获取或设置一个元素内容的滚动尺寸。

image

element.scrollTop = 100;
element.scrollLeft = 100;

4. scrollWidth / scrollHeight

获取元素实际内容区域的宽高,包括隐藏部分,但是不包括 border 和滚动条。

image

element.scrollWidth = element.scrollLeft + element.clientWidth;
element.scrollHeight = element.scrollTop + element.clientHeight;

5. offsetParent(只读)

计算元素的 offsetWidth 与 offsetHeight 值时,获取元素的第一个定位祖先元素。

Tip: document.body 与 CSS属性 position 值为 fixed 的元素,其 offsetParent 为 null。

6. offsetTop / offsetLeft(只读)

获取 element 上边界和左边界相对于 element.offsetParent 的偏移量。

image

7. offsetWidth / offsetHeight(只读)

获取元素视口的整体宽高,也就是连带 border、padding 和滚动条在内的宽高。

image

element.offsetWidth = element.getBoundingClientRect().width;
element.offsetHeight = element.getBoundingClientRect().height;

Tip: 对于 document.documentElement 元素,在IE11 以下的浏览器里,offsetWidth / offsetHeight 获取到的宽高是当前可视窗口 包含滚动条 在内的整体宽高,而不是 html 元素的整体宽高。

MouseEvent 位置属性

鼠标事件对象也包含一些与位置相关的属性,包括:

1. MouseEvent.screenX / MouseEvent.screenY
2. MouseEvent.pageX / MouseEvent.pageY
3. MouseEvent.clientX / MouseEvent.clientY
4. MouseEvent.offsetX / MouseEvent.offsetY
5. MouseEvent.x / MouseEvent.y

1. MouseEvent.screenX / MouseEvent.screenY(只读)

获取鼠标相对于屏幕坐标系的水平和垂直方向的偏移量。

Tip: IE浏览器是根据屏幕分辩率来算的,而其余现代浏览器则是根据视口的实际尺寸来计算的,比如我的浏览器分辨率是1900px,但由于把设置了显示效果,实际视口尺寸只有1280px。

2. MouseEvent.pageX / MouseEvent.pageY(只读)

获取鼠标相对于整个文档坐标系的水平和垂直方向的偏移量。

Tip: 在IE浏览器中,假如 html 元素存在 margin,那么计算时会减去 margin 值,在其余现代浏览器中则不会。

3. MouseEvent.clientX / MouseEvent.clientY(只读)

获取鼠标相对于浏览器视口坐标系的水平和垂直方向的偏移量。

4. MouseEvent.offsetX / MouseEvent.offsetY(只读)

获取鼠标相对于目标元素内填充边坐标系的水平和垂直方向的偏移量。

最后

对于 body 和 body 内的普通元素而言,以上叙述并没有问题,但如果涉及到 document.documentElement,不同浏览器以及IE自己的不同版本间都存在一定的差异,这里并没有做详细记录,因为实在不知道该怎么记录。

要抹平差异也简单,只需要记住,不要给 html 元素添加 margin 和 border 属性。

JavaScript 进阶 - 基本类型数据

7 个基本数据类型

随着 ES2020 的正式发布,JavaScript 的基本数据类型已经新增到 7 个,分别是 nullundefinedbooleanstringnumbersymbolbigint

前面 5 个基本数据类型比较简单,这里就不做赘述。我会主要讲讲最近几个规范才新增的 symbolbigint

symbol

首先看看如何在代码里定义一个 symbol 类型的数据:

const sym1 = Symbol();

// 还可以添加一个字符串,作为描述
const sym2 = Symbol('hello');

我们还可以通过 description 属性获取描述

sym2.description // hello

Symbol() 函数会返回 symbol 类型的值,该类型具有静态属性和静态方法。每个从 Symbol() 返回的symbol值都是 唯一 的。

const sym1 = Symbol();
const sym2 = Symbol();

typeof sym1 === 'symbol'; // true
sym1 === sym2; // false

注意:Symbol() 前面不要带 new 关键字,否则会报错

symbol 类型的值作为对象属性,不会被转换成字符串:

// 用数字作为对象的 key
const num = 1;
const str = '1';
const obj1 = {
    [num]: 'number key'
};
// 通过字符串依然可以访问
obj1[num] === obj1[str]; // true

// 用 Symbol() 创建的值不会被转换成字符串,始终唯一
const sym1 = Symbol();
const sym2 = Symbol();
const obj2 = {
    [sym1]: 'i am sym1'
};
obj2[sym1] === obj2[sym2]; // false

另外,对象中 symbol 类型的属性还具有”隐藏“的特性:

const sym = Symbol();
const obj = {
    name: '学长',
    [sym]: 'i am a symbol'
};
Object.keys(obj); // ['name']

// 使用 `for...in` 循环也找不到
// 不过可以用 `in` 操作符进行判断
sym in obj; // true

// 也可以使用 Object.getOwnPropertySymbols() 获取
Object.getOwnPropertySymbols(obj); // [Symbol()]

还有 Symbol.for(key)Symbol.kefFor(sym) 方法,因为目前没啥用处,所以这里不做过多介绍。

bigint

bigint 是一个新的基本数据类型,用来表示大于 2^53 - 1 的整数。

我们可以调用 BigInt() 函数,或者直接在数字后面添加一个 n 来定义 bigint 类型的数据。

const big1 = BigInt(10);
const big2 = 10n;

typeof big1; // bigint
big1 === big2; // true

bigint 类型的数据也只能和 bigint 类型的数据进行运算,并且也不能用于 Math 对象中的方法:

1n + 2n; // 3n

1n + 2; // TypeError: Cannot mix BigInt and other types, use explicit conversions

Math.pow(2n,3); // TypeError: Cannot convert a BigInt value to a number

bigint 类型的数据进行运算时,如果有小数,会自动向下取整:

5 / 2; // 2.5
5n / 2n; // 2n

11 / 3; // 3.6666666666666665
11n / 3n; // 3n

注意,不要在 JSON 中使用 bigint 类型的数据,否则会抛错:

JSON.stringify(BigInt(1)); // TypeError: Do not know how to serialize a BigInt

CSS实现元素水平居右

如何实现下图所示的元素居右效果?
image

网上有很多关于元素水平居中、垂直居中的文章,却少有水平居右或者垂直居底的方法。

但是在实际工作中,元素右对齐以及居底的需求也并不少。

这篇文章就讲讲如何实现元素的水平居右。

内联元素

内联元素的右对齐很简单,只需要在其父元素上添加 text-align: right; 即可。

不过值得注意的是, text-align: right; 对于内联块级元素,比如 img 等同样适用。

代码:

<div class="out">
    <img src="./img/header1.jpg" alt="" class="in">
</div>
.out {
    background-color: cornsilk;
    text-align: right;
}
.in {
    width: 100px;
    height: 100px;
}

效果:
image

块级元素

块级元素的水平居右方式就有好几种了。

方式一:右浮动

大家之所以没太关注块级元素水平居右,应该很大程度上也是因为浮动,以为一行 float: right; 便能搞定。

但浮动会造成父元素高度的坍塌,所以在使用 float 属性时,往往我们还需要清楚浮动带来的副作用。

方式二:绝对定位

给元素设置 postion: absolute; right: 0; 也可以实现水平居右,但由于绝对定位,元素脱离了标准文档流。

为了避免给后面元素的位置造成影响,往往还添加一个相对定位的父元素,并且父元素需要知道子元素的高度。

代码:

<div class="demo">
    <h3>通过 postion 属性实现:</h3>
    <div class="box2">
        <img src="./img/header1.jpg" alt="girl" class="item">
        <div class="out">
            <img src="./img/header2.jpg" alt="girl" class="item p-right">
        </div>
        <img src="./img/header3.jpg" alt="girl" class="item">
        <div class="out">
            <img src="./img/header4.jpg" alt="girl" class="item p-right">
        </div>
    </div>
</div>
.demo {
    width: 600px;
    margin: 0 auto;
}
.box2 {
    background-color: bisque;
}
.item {
    width: 100px;
    height: 100px;
    margin-bottom: 10px;
}
.item:last-child {
    margin-bottom: 0;
}
.out {
    position: relative;
    height: 100px;
}
.p-right {
    position: absolute;
    right: 0;
}

方式三:flex 布局

以前需要掌握各种技巧才能实现的复杂布局,通过 flex 都可以轻松实现,所以元素水平居右这种需求,对于 flex 来说也是小菜一碟。

代码:

<div class="demo">
    <h3>通过 flex 布局实现:</h3>
    <div class="box1">
        <img src="./img/header1.jpg" alt="girl" class="item">
        <img src="./img/header2.jpg" alt="girl" class="item flex-end">
        <img src="./img/header3.jpg" alt="girl" class="item">
        <img src="./img/header4.jpg" alt="girl" class="item flex-end">
    </div>
</div>
.demo {
    width: 600px;
    margin: 0 auto;
}
.box1 {
    display: flex;
    flex-direction: column;
    background-color: bisque;
}
.item {
    width: 100px;
    height: 100px;
    margin-bottom: 10px;
}
.item:last-child {
    margin-bottom: 0;
}
.flex-end {
    align-self: flex-end;
}

效果:
image

方式四:margin 与 auto

很多人天天都在使用 margin: 0 auto; 来实现块级元素居中,但却不知道 auto 这个值在 margin 属性中的具体表现。

我们将块级元素的某一个水平方向的 margin 值设置为 auto,那么它就会自动填充剩余空间。

如果两个水平方向的 margin 值都为 auto,那么元素就会居中。

代码:

<h3>通过 margin 属性实现:</h3>
<div class="box2">
    <img src="./img/header1.jpg" alt="girl" class="item">
    <img src="./img/header2.jpg" alt="girl" class="item ml-auto">
    <img src="./img/header3.jpg" alt="girl" class="item">
    <img src="./img/header4.jpg" alt="girl" class="item ml-auto">
</div>
.demo {
    width: 600px;
    margin: 0 auto;
}
.box2 {
    background-color: bisque;
}
.item {
    display: block;
    width: 100px;
    height: 100px;
    margin-bottom: 10px;
}
.item:last-child {
    margin-bottom: 0;
}
.ml-auto {
    margin-left: auto;
}

效果:
image

总结

方式一和方式二虽然都能实现元素水平居右,但都需要更多的代码去处理其副作用。而 flex 布局和 margin + auto 的方式则更显完美。

在需求比较简单时,推荐使用 margin-left: auto; 的方式,一行代码搞定。

布局要求复杂时,那么更强大的 flex 布局则是最佳选择。

至于更强大的 grid 布局,由于一些老版本的360浏览器、qq浏览器等,支持并不是很完善,用户数又较大,所以暂时不做介绍。

基于Vue组件的CSS命名规范

CSS命名规范

每个 Vue 组件的最外层容器采用 组件名+root 的方式命名,如:

<template>
    <div class="paging-table-root">
        ...
    </div>
</template>

这样做是为了更快速定位一个组件,而不用先通过 vue-devtool 找到组件名,然后再去项目里找。

其余元素的命名按照下面的规则:

1、命名规则

按照 B--E-M 的书写格式,详情可见BEM介绍

B: 代表 Block

每个 Block 都由多个 Element 组成,应尽量保证每个组件只有一个 Block:

<template>
    <div class="paging-root">
        <ul class="paging-list">
            <li class="paging-item">
                <a class="paging-btn">上一页</a>
            </li>
            ...
            <li class="paging-item">
                <a class="paging-link">5</a>
            </li>
            ...
            <li class="paging-item">
                <a class="paging-btn">下一页</a>
            </li>
        </ul>
    </div>
</template>

当一个 Block 实在无法满足使用的时候,再将该 Block 按结构划分为多个部分,这几个部分作为其它具体元素的 Block。

<template>
    <div class="article-root">
        <div class="article-header">
            <ul class="header-list">
                <li class="header-item"></li>
            </ul>
        </div>
        ...
        <div class="article-main">
            <ul class="main-list">
                <li class="main-item"></li>
            </ul>
        </div>
    </div>
</template>

E: 代表 Element

每一个 Element 都应该具有独立性,可以用在 Block 的任何地方,而不需要做额外处理。

<template>
    <div class="comment-root">
        ...
        <div class="comment-header">
            <p class="comment-txt">重要的是命名的**,而不是完全照搬。</p>
        </div>
        ...
        <div class="comment-main">
            <p class="comment-txt">赞同!</p>
        </div>
    </div>
</template>

另外每一个 Element 都可能存在不同的状态,比如选中状态,激活状态,以及大小颜色等。

<template>
    <div class="login-root">
        ...
        <input class="login-input" type="text">
        <input class="login-input login-input-error" type="text">
        ...
    </div>
</template>

M: 代表 Modifier

Modifier 是 Element 的修饰符,用来区分 Element 可能存在不同的状态。

在一些特殊情况下,通常是 UI 做的不够标准统一时候,单一的 Element 内外边距不能即满足独立可复用性,又满足 UI 样式。这时 Modifier 也可以用来表示元素的容器,比如:

<template>
    <div class="login-root">
        ...
        <div class="login-form-wrap">
            <form class="login-form">
                ...
            </form>
        </div>
    </div>
</template>

2、书写规则

每个组件的 <style> 标签都需要添加 scoped 属性:

<style scoped>
    ...
</style>

选择器之间禁止嵌套,便于代码阅读和后期维护,以及避免权重所带来的影响:

<!-- 好的写法 -->
<style scoped>
    .news-list {}
    .news-item {}
    .news-txt {}
</style>
<!-- 不允许的写法 -->
<style scoped>
    .news-list {}
    .news-list .news-item {}
    .news-item .news-txt {}
</style>

css样式书写时按照定位、盒模型、样式、功能的顺序,比如:

<style scoped>
    .news-list {
        /* 定位 */
        position: absolute;
        left: 0;
        z-index: 10;

        /* 盒模型 */
        float: left;
        display: block;
        flex: 1;
        align-items: center;
        width: 0;
        height: 0;
        padding: 0;
        margin: 0;
        border: 0;
        outline: 0;

        /* 样式 */
        background: #fff;
        color: #fff;
        font: italic bold 12px/20px arial,sans-serif;

        /* 功能 */
        transition: all 1s;
        transform: translate(10px, 10px);
        cursor: pointer;
    }
</style>

3、命名常用词汇

Block 与 Element 之间不需要进行层层嵌套,父级的 Element 也可以作为子级的 Block,这样能避免当元素嵌套较深时,class 需要写很长一串的问题。

常用块(Block)

结构类:header, nav, subnav, menu, submenu, main, aside, footer

内容类:summary, banner, article, login, register, form, news

对于一个简单的组件,优先使用组件名作为 Block,不用把组件名全写出来,选其中一个典型单词就可以,比如:

<div class="morning-news-root">
    <div class=news-header>
        <p class="news-title">早间新闻</p>
        <a class="news-btn" href="#">更多</a>
    </div>
    <ul class="news-list">
        <li class="news-item">特普朗退出TTP</li>
    </ul>
</div>

常用元素(Element)

结构类:header, main, content, footer

文本类:txt, link,

表单类:form, input, label

表格类:table, column, row, cell

列表类:list, item, filed

按钮:button

常用修饰符(Modifier)

状态类:primary, success, warning, error, active

形状类:large, big, small

样式类:fl, center, middle, fr

容器类:box, wrap

Vue CLI 3.0 说明

本文对 Vue CLI 3.0 文档一些可能不明确的地方做一些说明,本文的所有内容都是基于 Vue CLI v3.0.0-rc.10。

~/.vuerc

第一次使用 Vue CLI 创建项目的时候,在操作提示的最后,你可以选择保存一个将来可服用的 preset,这个 preset 就保存在 home 目录下的 .vuerc 文件里。

而这个 home 目录就是你打开 CMD 时显示的目录 C:\Users\XXX

image

windows 用户还可以用 PowerShell 工具,输入 cd ~/.vuerc 命令进行查看:

image

vue.config.js

该文件在创建工程时是不存在的,需要手动创建,具体如何配置,参考文档。

index.html

`public/index.html` 文件是一个会被 html-webpack-plugin 处理的模板。

......

因为 index 文件被用作模板,所以你可以使用 lodash template 语法插入内容

上面是文档里对于 public/index.html 的一部分描述,但文档没说的是我们可以在 index.html 中直接使用 ejs 的模板语法:

image

现代模式

在打包的时候,推荐大家都使用现代模式,现代模式打包会生成两份代码,非兼容包会比兼容包体积小很多:

image

能识别 type="module" 属性的浏览器,会自动忽略掉带有 nomodule 属性的 script 标签:

image

不过在 IE 中,这种方式就会加载两份 JS 文件,因为所有 IE 都不支持 ES2015+ 的语法:

image

所以对于那些还想需要给 IE 用户带来良好体验的公司,这种方法注定就只能抛弃了。

注意: Edge 浏览器支持 ES2015+,但也不支持 nomodule 属性

你可能不清楚的 CSS 选择器用法(二)

子元素选择器

用法:parent > child { }

子元素选择器通过 > 符连接两个元素,两个元素是严格的父子关系。

例子:

  <div class="parent">
    <p class="child">我是子元素.child</p>
    <p class="child">我是子元素.child</p>
    <div class="box">
      <p class="child">我是孙子元素.child</p>
    </div>
  </div>
.parent > .child {
  color: #f23;
}

image

后代元素选择器

用法:parent child { }

后代元素选择器通过一个空格符连接两个元素,child 元素可以是 parent 元素的子元素,也可以是孙子元素,曾孙子元素,曾曾曾。。。

例子:

  <div class="parent">
    <p class="child">我是子元素.child</p>
    <p class="child">我是子元素.child</p>
    <div class="box">
      <p class="child">我是孙子元素.child</p>
    </div>
  </div>
.parent  .child {
  color: #f23;
}

image

通用兄弟选择器

用法:prev ~ nextAll { }

通过 ~ 符连接两个同级元素,nextAll 元素是 prev 后的所有同级兄弟元素。

这个选择器大家用的肯定很少,但是它其实相当强大,在不可思议的纯CSS导航栏下划线跟随效果这篇文章中,实现核心其实就是使用通用兄弟选择器 ~

它其实还可以在更多的地方来简化我们的效果实现方式,比如我们要实现一个如下的tab页切换的效果:

tab1

通常我们可能直接想到的就是完全使用JavaScript,获取 tab 标签按钮,获取滑动块,通过索引判断,写动画,还得判断方向......想想就麻烦,但是如果配合 ~ 选择器,我们的JavaScript代码就会大量减少。

我就结合效果来逐一分析代码了,全部上上来,大家自己看吧:

  <div id="box">
    <div class="tab-list">
      <div class="tab-item tab-item-active">标签一</div>
      <div class="tab-item">标签二</div>
      <div class="tab-item">标签三</div>
      <div class="tab-item">标签四</div>
    </div>
    
    <div class="swiper-list">
      <div class="swiper-item swiper-item-active bg-red">第一页内容</div>
      <div class="swiper-item bg-yellow">第二页内容</div>
      <div class="swiper-item bg-blue">第三页内容</div>
      <div class="swiper-item bg-green">第四页内容</div>
    </div>
  </div>
#box {
  width: 600px;
  height: 400px;
  border: 1px solid #ccc;
}

/* tab标签栏样式 */
.tab-list {
  display: flex;
  justify-content: space-between;
  border-bottom: 1px solid #ccc;
}
.tab-item {
  flex: 1;
  text-align: center;
  line-height: 50px;
  cursor: pointer;
}
.tab-item:hover {
  background-color: #3f3;
}
.tab-item-active {
  background-color: #3f3;
}

/* 滑动块的样式 */
.swiper-list {
  box-sizing: border-box;
  position: relative;
  width: 600px;
  height: 349px;
  overflow: hidden;
}
/* 下面三个样式规则是实现这个效果的核心 */
.swiper-item {
  position: absolute;
  left: -100%;
  width: inherit;
  height: inherit;
  transition: all .3s;
}
.swiper-item-active {
  left: 0;
}
.swiper-item-active ~ .swiper-item {
  left: 100%;
}

/* 滑动内容的背景色 */
.bg-red {
  background-color: red;
}
.bg-yellow {
  background-color: yellow;
}
.bg-blue {
  background-color: blue;
}
.bg-green {
  background-color: green;
}
const tabItem = document.querySelectorAll('.tab-item');
const swiperItem = document.querySelectorAll('.swiper-item');
tabItem.forEach((item, index) => {
  item.onclick = function () {
    const tabActive = document.querySelector('.tab-item-active');
    tabActive.classList.remove('tab-item-active')
    item.classList.add('tab-item-active');
    
    const swiperActive = document.querySelector('.swiper-item-active');
    swiperActive.classList.remove('swiper-item-active')
    swiperItem[index].classList.add('swiper-item-active')
  }
})

你可能跟我一样,能不用JS的地方就坚决不用JS,所以针对上面的情况,能不能一行JS都不写呢?也不是没有办法。

相邻兄弟选择器

用法:prev + next { }

通过 + 符连接两个同级元素,next 元素是紧挨着 prev 的同级兄弟元素。

伪类选择器 :checked

用法:input:checked { }

表示被选中的 radiocheckboxselect 元素。

结合这两个选择器,我们对上面的代码进行修改:

  <div id="box">
    <!--  这里通过使用label元素来选中radio  -->
    <div class="tab-list">
      <label class="tab-item-label" for="item-radio-1">标签一</label>
      <label class="tab-item-label" for="item-radio-2">标签二</label>
      <label class="tab-item-label" for="item-radio-3">标签三</label>
      <label class="tab-item-label" for="item-radio-4">标签四</label>
    </div>
    
    <div class="swiper-list">
      <input type="radio" name="item-radio" class="item-radio" id="item-radio-1" checked>
      <div class="swiper-item swiper-item-active bg-red">第一页内容</div>
      <input type="radio" name="item-radio" class="item-radio" id="item-radio-2">
      <div class="swiper-item bg-yellow">第二页内容</div>
      <input type="radio" name="item-radio" class="item-radio" id="item-radio-3">
      <div class="swiper-item bg-blue">第三页内容</div>
      <input type="radio" name="item-radio" class="item-radio" id="item-radio-4">
      <div class="swiper-item bg-green">第四页内容</div>
    </div>
  </div>
#box {
  width: 600px;
  height: 400px;
  border: 1px solid #ccc;
}

/* tab标签按钮样式 */
.tab-list {
  display: flex;
  justify-content: space-between;
  border-bottom: 1px solid #ccc;
}
.tab-item-label {
  position: relative;
  flex: 1;
  text-align: center;
  line-height: 50px;
  cursor: pointer;
}
.tab-item-label:hover {
  background-color: #3f3;
}

/* 滑动块样式 */
.swiper-list {
  box-sizing: border-box;
  position: relative;
  width: 600px;
  height: 349px;
  overflow: hidden;
}
.swiper-item {
  position: absolute;
  left: -100%;
  width: inherit;
  height: inherit;
  transition: all .3s;
}
/* 隐藏input元素 */
.item-radio {
  display: none;
}
/* 点击label元素,对应被选中的input后的那个.swiper-item */
.item-radio:checked + .swiper-item {
  left: 0;
}
/* 选中的input后的那个.swiper-item后的所有同级.swiper-item */
.item-radio:checked + .swiper-item ~ .swiper-item {
  left: 100%;
}

/* 滑动内容背景色 */
.bg-red {
  background-color: red;
}
.bg-yellow {
  background-color: yellow;
}
.bg-blue {
  background-color: blue;
}
.bg-green {
  background-color: green;
}

效果如下:

tab2

不过这里有个缺陷,就是 tab标签 按钮没有实现选中效果,我暂时还没有想到好的办法,大家可以集思广益,如果有好的实现方式,请一定在评论告诉大家,谢谢!

总结

写了两篇 CSS选择器 的文章,但没有把所有选择器写出来,我也并没有把规范抄一次的打算。这里只写了几个容易混淆的选择器和我暂时发现用的很少,但能力很强的选择器,以后可能还会继续新增,特别是对于伪类选择器和伪元素的使用。

对于新手可以举一反三,对于大牛,就当做抛砖引玉好了,希望能看到大家更多实用的CSS选择器技巧。

题解JavaScript作用域

又到一年底,大家是不是又开始蠢蠢欲动,准备开始到处吹嘘了。

不过你确定你已经准备好了吗?

image

凡是参加过面试的朋友应该都有过一个相同的感受,就是面试官常常会抛出个很宏观的问题,让你有种 狗啃乌龟,不知从何下口 的赶脚。

image

现在让我们来一道最基本的题:能不能说说你对JavaScript作用域的理解?

image

我记得之前问过一个从事开发十来年的大哥,大哥贼猛,前后端通杀,运维也照样搞,撸码就是一把梭,抡起jQuery就是干。。。跑题了。。。

当时他给我的回答大体是:

  JavaScript是门动态语言,跟Java不一样,JavaScript可以随意定义全局变量和局部变量,变量会在该作用
  域下提升,而且JavaScript没有块级作用域。全局变量就是定义在全局的变量了,局部变量是定义在函数
  里的变量,每一个函数都是一个作用域,当函数执行时会优先查找当前作用域,然后逐级向上。定义在 
  if 和 for 语句里的变量,在大括号外面也能访问到,这就是没有块级作用域。

如果你正在阅读这篇文章,你或许不能从这篇文章学到什么东西,但起码说明你是个愿意学习的人,所以我觉得你可能会在上面回答的基础上加一句:JavaScript是静态作用域,在对变量进行查询时,变量值由函数定义时的位置决定,和执行时的所处的作用域无关

知道ES6的童鞋可能还会指出 JavaScript已经有块级作用域了,而且用 let 和 const 定义的变量不会提升

到此为止,似乎JavaScript的作用域和作用域链都简单概括完了,但是,你真的没有问题了吗?

image

// 第一题
var a = 1;
function fn() {
  console.log('1:' + a);

  var a = 2;
  bar()
  console.log('2:' + a)
}

function bar() {
  console.log('3:' + a)
}

fn()
// 第二题
var a = 1;
function fn() {
  console.log('1:' + a);
  a = 2
}

a = 3;
function bar() {
  console.log('2:' + a);
}

fn();
bar();

大家先猜猜,这两道题打印的值分别是多少。

把你自己分析出来的值记下来,再和正确答案对比下,看看对了多少。

// 第一题正确答案
1:undefined
3:1
2:2

// 第二题正确答案
1:3
2:2

我们先看 第一题

第一个 a 打印的值是 1:undefined 而不是 1。因为我们在 fn() 中定义了变量 a,用 var 定义的变量会在当前作用域提升,但是并不会携带赋给变量的值一起提升。

第二个 a 打印的值是 3:1 而不是 2。因为函数 bar 是定义在全局作用域中的,所以作用域链是 bar -> global,bar 里面没有定义a,所以就会顺着作用域链向上找,然后在 global 中找到了 a。

第三个 a 打印的值是 2:2。这句话所在的作用域链是 fn -> global,执行 console.log('2:' + a) 会首先在 fn 作用域里查找 a,找到有 a,并且值为2,所以结果就是2。

再来看看 第二题

第一个 a 打印的值是 1:3,既不是 undefined 也不是 1。首先, fn 中的 a = 2 是给变量 a 赋值,并没有定义变量。然后,执行函数 fn,在查找变量 a 时,此时查找的变量就是全局变量 a,不过此时 a 的值为3。

第二个 a 打印的值是 2:2。函数 bar 所能访问的作用域链为 bar->global,在执行函数 bar 时,a 的值已经被修改成了 2。

结论

前面的东西可能有点啰嗦冗余,我们稍稍提炼总结一下。

  1. 在JavaScript中,通过 letconst 定义的变量具有块级作用域的特性。

  2. 通过 var 定义的变量会在它自身的作用域中进行提升,而 letconst 定义的变量不会。

  3. 每个JavaScript程序都具有一个全局作用域,每创建一个函数都会创建一个作用域。

  4. 在创建函数时,将这些函数进行嵌套,它们的作用域也会嵌套,形成作用域链,子作用域可以访问父作用域,但是父作用域不能访问子作用域。

  5. 在执行一个函数时,如果我们需要查找某个变量值,那么会去这个函数被 定义 时所在的作用域链中查找,一旦找到需要的变量,就会停止向上查找。

  6. “变量的由函数定义时的位置决定”这句话有歧义,准确说是查找变量时,是去定义这个函数时所在的作用域链查找。

小练习

之前本来没想过写这篇文章,只是前段时间在翻译 什么是JavaScript闭包? 这篇文章时,做里面的一道题时竟然错了一个值,所以写了这篇文章,没有什么新东西。算是为了提醒像我一样还没有称谓老司机的童鞋们,我们可能看了很多文章很多书,知道了很多理论,但是在分析代码和问题时,一定要仔细全面。

最后给一些新入坑的童鞋留个小作业,判断打印出来的 a 的值是多少:

// 第一题
var a = 1;
function fn(f) {
  var a = 2;
  return f;
}
function bar() {
  console.log(a)
}
var f1 = fn(bar);
f1()
// 第二题
var a = 1;
function fn(f) {
  var a = 2;
  return function () {
    console.log(a)
  }
}

var f1 = fn();
f1()

你可能不清楚的 CSS 选择器用法(一)

这是一篇由一个 CSS 选择器引发的文章,讲真,从来没想过会写一篇关于 CSS 选择器的文章。

事先声明,这篇文章是对一些基础的回顾,没有什么新东西或者黑科技。
如果你早已将这些 CSS 基础烂熟于胸,大可直接 Ctrl + w。
如果你跟我一样对这些基础有些遗忘,那么可以尝试看下去。

事情的经过是这样的......

那是一个月黑风高的夜晚,我和同事一边愉快地加着班,一边悲愤地打着农药。趁着被敌方后羿shutdown的间隙,我花了 20 秒敲下了一行代码,等待页面热更新,查看效果,perfect!一切正常!

然鹅,正当我快要复活的时候,列表也滚到底了,卧槽!!最后一个 Item 样式出现了问题,但是时间已经来不及,快要复活了......以后可以写一篇博客叫《论敌方打野与我方打野》。

虽然我只花了一分钟就换了个方式来解决这个问题,但彻底把原来的问题搞明白却花了我差不多一个小时。

先给大家重现一下问题:

之前正常运行的代码

	<div class="list">
		<div class="item">我是第一个item</div>
		<div class="item">我是第二个item</div>
		<div class="item">我是第三个item</div>
		<div class="item">我是第四个item</div>
		<div class="item">我是第五个item</div>
	</div>
	.list {
		padding: 15px;
		background-color: #eee;
	}
	.item {
		margin-bottom: 15px;
		border-radius: 4px;
		background-color: #fff;
	}
	.item:last-child {
		margin-bottom: 50px;
	}

有问题的代码

	<div class="list">
		<div class="item">我是第一个item</div>
		<div class="item">我是第二个item</div>
		<div class="item">我是第三个item</div>
		<div class="item">我是第四个item</div>
		<div class="item">我是第五个item</div>
		<div class="dialog">......</div>
	</div>
	.list {
		padding: 15px;
		background-color: #eee;
	}
	.item {
		margin-bottom: 15px;
		border-radius: 4px;
		background-color: #fff;
	}
	.item:last-child {
		margin-bottom: 50px;
	}

发现问题在哪了吗?
如果你一眼看出问题所在,并确切的知道原因,那么恭喜你,CSS 基础还是挺扎实的。
如果你发现了问题但不确定原因或者压根都没找到问题,那么也同样恭喜你,至少看见了这篇文章。

:last-child

通常用法:element:last-child

匹配 element 父元素及其父元素的子元素,中的最后一个 element 子元素。

注意:这里的 element 元素必须是其父元素下 所有元素 的最后一个子元素才有效。

例子:

	<div class="list">
		<div class="item">我是第一个item</div>
		<div class="item">我是第二个item</div>
		<div class="item">我是第三个item</div>
		<div class="box">
			<div class="item">穿插一个盒子item</div>
			<div class="item">穿插两个盒子item</div>
			<div class="item">穿插三个盒子item</div>
		</div>
		<div class="item">我是第四个item</div>
		<div class="item">我是最后一个item,但不是最后一个子元素</div>
		<div class="haha">我才是最后一个子元素</div>
	</div>
	.list {
		padding: 15px;
		background-color: #eee;
	}
	.item {
		margin-bottom: 15px;
		border-radius: 4px;
		background-color: green;
	}
	.item:last-child {
		background-color: red;
	}
	.box {
		margin-left: 50px;
	}
	.haha {
		background-color: yellow;
	}

image

:first-child 选择器与它用法一样

:last-of-type

通常用法:element:last-of-type

匹配 element 父元素下及其父元素的子元素,中的最后一个 element 子元素。

注意:这里的 element 元素只要是其父元素下的 所有 element 元素 中的最后一个就可以。

所以看到这里,你是不是会以为下面这段代码:

	<div class="list">
		<div class="item">我是第一个item</div>
		<div class="item">我是第二个item</div>
		<div class="item">我是第三个item</div>
		<div class="box">
			<div class="item">穿插一个盒子item</div>
			<div class="item">穿插两个盒子item</div>
			<div class="item">穿插三个盒子item</div>
		</div>
		<div class="item">我是第四个item</div>
		<div class="item">我是最后一个item,但不是最后一个子元素</div>
		<div class="haha">我才是最后一个子元素</div>
	</div>
	.list {
		padding: 15px;
		background-color: #eee;
	}
	.item {
		margin-bottom: 15px;
		border-radius: 4px;
		background-color: green;
	}
	.list .item:last-of-type {
		background-color: red;
	}
	.box {
		margin-left: 50px;
	}
	.haha {
		background-color: yellow;
	}

它的结果会是这样:

image

大错特错!

它的结果其实是这样:

image

让我们把上面 class="item"div 标签换成 p 标签,那么结果就是

image

image

明明是同样的 HTML 结构和 CSS 样式,为什么就换了一个标签类型,结果就不一样了呢?

我在规范文档里发现了这样两句话:

Pseudo-classes are allowed in all sequences of simple selectors contained in a selector. Pseudo-classes 
are allowed anywhere in sequences of simple selectors, after the leading type selector or universal 
selector (possibly omitted).
:first-of-type pseudo-class
Same as :nth-of-type(1). The :first-of-type pseudo-class represents an element that is the first sibling of its type.

其大致意思是:

在一个选择器中,所有包含[简单选择器](https://drafts.csswg.org/selectors-3/#simple-selectors)的序列中都允许伪类。在简单选择器序列中,伪类可以放在任何地方,在主类型选择器后或者通配符选择器后(可能省略)。
:first-of-type 伪类
等同于 :nth-of-type(1)。 :first-of-type 伪类表示它这种类型的第一个兄弟元素。

不知道你是什么赶脚,反正我是

image

文字的表达和理解真的不是一件简单的事,所以我也只能大概的总结一下,意思就是:

:last-of-type 这样的伪类,是可以跟在一个任意简单选择器后的,但除了要匹配这个简单选择器,同时还要匹配这个简单选择器的元素类型,也就是元素标签。

talk is cheap,show you the code:

	<div class="list">
		<p class="item">我是第一个p标签 .item</p>
		<p class="item">我是第二个p标签 .item</p>
		<p class="item">我是第三个p标签 .item</p>
		<div class="box">
			<p class="item">穿插一个盒子p标签 .item</p>
			<p class="item">穿插两个盒子p标签 .item</p>
			<p class="item">穿插三个盒子p标签 .item</p>
		</div>
		<p class="item">我是第四个p标签 .item</p>
		<p class="item">我是最后一个p标签 .item</p>
		<div class="item">我是最后一个 .item,但不是最后一个div元素</div>
		<div class="haha">我才是最后一个div元素</div>
	</div>
	.list {
		padding: 15px;
		background-color: #eee;
	}
	.item {
		margin-bottom: 15px;
		border-radius: 4px;
		background-color: green;
	}
	.list .item:last-of-type {
		background-color: red;
	}
	.box {
		margin-left: 50px;
	}
	.haha {
		background-color: yellow;
	}

image

总结

这一篇写完,连我自己都有些晕,所以大家还是要仔细看看代码,挨个敲一敲,然后再好好体会。

Vue.js 中,7种定义组件模板的方法 | Codementor

在vue中定义模板组件时存在有多种选择。我算了一下,至少有7种不同的方法:

  • String

  • Template literal

  • X-Templates

  • Inline

  • Render functions

  • JSX

  • Single page components

当然,可能还会有更多方法!

在这篇文章里,我们将会展示每一个示例,并且分析其优缺点,以便你能明白在对应的情形下,哪种方式是合适的。

注意: 这篇文章最初发表在 这里 Vue.js Developers blog on 2017/03/24

1. Strings

默认情况下,模板会作为一个字符串被定义在你的JS文件里。但是我觉得大家都同意这种写法是难以看懂的,除了广泛的浏览器支持之外,这种方法并没有太多的好处。

Vue.component('my-checkbox', {
  template: '<div><div></div><div>{{ title }}</div></div>',
  data() {
    return { checked: false, title: 'Check me' }
  },
  methods: {
    check() { this.checked = !this.checked; }
  }
});

2. Template literals(模板字符串)

通过ES6的模板字符串(反引号)语法,你在定义模板时可以直接换行,这是你通过常规的JavaScript字符串没法做到的。
这种写法更容易阅读,并且这种模板字符串语法得到了许多新版本浏览器的支持。当然,为了安全起见,你仍然应该把它转译为ES5的语法形式。

然而,这种方式并不完美,我发现大多数的IDE在语法高亮上做的差强人意,并且缩进和换行等,仍然是个痛。

Vue.component('my-checkbox', {
  template: `<div>
              <div></div>
              <div>{{ title }}</div>
            </div>`,
  data() {
    return { checked: false, title: 'Check me' }
  },
  methods: {
    check() { this.checked = !this.checked; }
  }
});

3. X-Templates

使用这种方法,你需要在index.html文件里的script标签中定义你的模板。script标签需要添加text/x-template类型作为标记,并且在定义组件时,通过id来引用。

我喜欢这种方式,它允许你使用适当的HTML标记来书写你的HTML文件,但是不足之处在于,这种方式会把模板和组件的其它定义分开了。

Vue.component('my-checkbox', {
  template: '#checkbox-template',
  data() {
    return { checked: false, title: 'Check me' }
  },
  methods: {
    check() { this.checked = !this.checked; }
  }
});

<script type="text/x-template" id="checkbox-template">
  <div>
    <div></div>
    <div>{{ title }}</div>
  </div>
</script>

4. Inline Templates(内联模板)

通过给组件添加inline-template属性来告诉Vue,在其里面的内容就是模板,而不是把它当作是分发内容(见 slots)。

它的缺点和x-templates一样,但是有一个优点就是,这种写法是在HTML模板对应的位置,所以它在页面一加载就渲染,而不用等到JavaScript执行。

Vue.component('my-checkbox', {
  data() {
    return { checked: false, title: 'Check me' }
  },
  methods: {
    check() { this.checked = !this.checked; }
  }
});

<my-checkbox inline-template>
  <div>
    <div></div>
    <div>{{ title }}</div>
  </div>
</my-checkbox>

5. Render functions(渲染函数)

渲染函数需要你把模板当作一个JavaScript对象来进行定义,它们是一些复杂并且抽象的模板选项。

然而,它的优点是你定义的模板更接近编译器,你可以使用所有JavaScript方法,而不是指令提供的那些功能。

Vue.component('my-checkbox', {
  data() {
    return { checked: false, title: 'Check me' }
  },
  methods: {
    check() { this.checked = !this.checked; }
  },
  render(createElement) {
    return createElement(
      'div',
        {
          attrs: {
            'class': 'checkbox-wrapper'
          },
          on: {
            click: this.check
          }
        },
        [
          createElement(
            'div',
            {
              'class': {
                checkbox: true,
                checked: this.checked
              }
            }
          ),
          createElement(
            'div',
            {
              attrs: {
                'class': 'title'
              }
            },
            [ this.title ]
          )
        ]
    );
  }
});

6. JSX

Vue中最有争议性的模板选项就是JSX,一些开发者认为JSX语法太丑,不直观,而且和Vue的简洁特性相违背。

JSX首先需要编译,因为浏览器并不支持JSX。但是如果你需要使用渲染函数,那么JSX语法绝对是一种更简洁的定义模板的方法。

Vue.component('my-checkbox', {
  data() {
    return { checked: false, title: 'Check me' }
  },
  methods: {
    check() { this.checked = !this.checked; }
  },
  render() {
    return <div>
             <div></div>
             <div>{ this.title }</div>
           </div>
  }
});

7. Single File Components(单文件组件)

只要在你的配置中使用的是合适的构建工具,那么单文件组件绝对是这些方法中的首选。它们有两个最好的优点:允许你使用标记,同时把所有组件定义都写在一个文件中。

单文件组件需要编译,并且一些IDE不支持这种类型文件的语法高亮,否则它难以被其它方法打败。

<template>
  <div>
    <div></div>
    <div>{{ title }}</div>
  </div>
</template>
<script>
  export default {
    data() {
      return { checked: false, title: 'Check me' }
    },
    methods: {
      check() { this.checked = !this.checked; }
    }
  }
</script>

你可能认为还有更多的方式来定义模板,因为你可能使用一些预编译模板,比如Pug.

哪个最好?

当然没有完美的方法,你应该根据你的实际案例来进行判断。我觉得一个好的程序员能知道所有可能性,并把它们当作Vue.js技术栈里一种解决问题的工具。

在 Electron 中使用 SQLite3

最近项目需要做一个简单的客户端,客户端需要保存各种数据,以便用户离线也能继续使用,当有网的时候再进行数据同步,所以这就需要客户端自带数据库的功能。

作为一个不会 C/C++ 的前端,在构建客户端的工具上肯定是选择 Electron 了,没有用 NW.js 的原因很简单,社区没有 Electron 大。

数据库方面,SQL 系的嵌入式数据库只有 SQLite 了,毕竟这么多年的老字号,经得住考验,比较靠谱。但是 Node 版的 SQLite 并不是纯 JavaScript 写的,而是原生 Node 模块,即包含 C/C++ 代码。

工具齐全,就差集成安装了,其它都挺顺利,但是在安装 SQLite 这一步,真的是让人吐血,从官网到百度里的各种方法试了个遍,但总会出现各种让你看不懂的错误,直到最后,还是得靠谷歌。

不BB了,直接写操作步骤:

1. 新建项目

看你心情,随便找个地方创建一个项目,其实就是个文件夹,然后执行:

npm init

结构目录如下:

sqlite-electron/
  ├── package.json
  ├── index.js
  ├── server.js
  └── index.html

2. 安装 Electron

npm install  electron -D

3. 安装 windows 下的相关环境

npm install --global --production windows-build-tools

详情参考 On Windows

4. 安装 node-gyp

npm install -g node-gyp

5. 构建 SQLite

npm install sqlite3 --build-from-source --runtime=electron --target=2.0.7 --dist-url=https://atom.io/download/electron

--target 参数的值是你安装的 electron 的版本

6. 安装 express

这个跟 SQLite 无关,是用来验证我们能否进行相关操作。

npm install express

7. 验证结果

我懒得写代码,直接从这些库的文档里拷贝代码:

// index.js


const {app, BrowserWindow} = require('electron')

// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let win

function createWindow() {
    // 创建浏览器窗口。
    win = new BrowserWindow({width: 800, height: 600})

    // 然后加载应用的 index.html。
    win.loadFile('index.html')

    // 打开开发者工具
    win.webContents.openDevTools()

    // 当 window 被关闭,这个事件会被触发。
    win.on('closed', () => {
        // 取消引用 window 对象,如果你的应用支持多窗口的话,
        // 通常会把多个 window 对象存放在一个数组里面,
        // 与此同时,你应该删除相应的元素。
        win = null
    })
}

// Electron 会在初始化后并准备
// 创建浏览器窗口时,调用这个函数。
// 部分 API 在 ready 事件触发后才能使用。
app.on('ready', createWindow)

// 当全部窗口关闭时退出。
app.on('window-all-closed', () => {
    // 在 macOS 上,除非用户用 Cmd + Q 确定地退出,
    // 否则绝大部分应用及其菜单栏会保持激活。
    if (process.platform !== 'darwin') {
        app.quit()
    }
})

app.on('activate', () => {
    // 在macOS上,当单击dock图标并且没有其他窗口打开时,
    // 通常在应用程序中重新创建一个窗口。
    if (win === null) {
        createWindow()
    }
})

// 在这个文件中,你可以续写应用剩下主进程代码。
// 也可以拆分成几个文件,然后用 require 导入。
// server.js


var express = require('express')
var sqlite3 = require('sqlite3').verbose();
var db = new sqlite3.Database(':memory:');
var app = express()

// 这段代码用来测试 SQLite
db.serialize(function() {
  db.run("CREATE TABLE lorem (info TEXT)");

  var stmt = db.prepare("INSERT INTO lorem VALUES (?)");
  for (var i = 0; i < 10; i++) {
      stmt.run("Ipsum " + i);
  }
  stmt.finalize();

  db.each("SELECT rowid AS id, info FROM lorem", function(err, row) {
      console.log(row.id + ": " + row.info);
  });
});

db.close();


// 这段代码用来测试 server 创建是否成功
app.get('/test', function(req, res) {
    res.json({
        code: 0
    })
})

var btn = document.getElementById('btn')
btn.onclick = function() {
    var server = app.listen(3000, function() {
        var host = server.address().address
        var port = server.address().port

        console.log('Example app listening at http://%s:%s', host, port)
    })
}
<!DOCTYPE html>
  <html>
    <head>
      <meta charset="UTF-8">
      <title>Hello Electron!</title>
    </head>
    <body>
      <h1>Hello Electron!</h1>
      <button id="btn">开启服务器</button>

      <script src="./server.js"></script>
    </body>
  </html>

package.json 中加入一条启动命令

"scripts": {
        "start": "electron ."
}

8. 见证奇迹

命令行中执行:

npm start

image

弹出了我们的应用程序!

点击 开启服务器 按钮:

image

成功打印启动 server 的信息!

在浏览器中输入 URL:

http://localhost:3000/test

image

成功返回数据!

9. 结尾

到这里我们的整个应用已经完成了,麻雀虽小,但五脏俱全。
打包应用我就不在这里写了,照着文档去弄就好了,目前一切顺利,并没有发现什么坑。
溜了~

用Node+puppeteer+wechaty每天定时给女(男)朋友发一张微信爱心提醒图

项目简介

通过微信每日定时给指定的一位好友发送消息,去年就有一个类似的想法,不过一直没去执行,直到上周看见篇文章。

不过他的一些功能我根本不需要,而且定时提醒消息只能是文字,看上去多少没那么好看,于是就打算自己撸一个,加了点其它的小功能,然后就有了这篇文章,前后也花了两天多时间,还有一些优化留着空了来做。

灵感来源

用Node+wechaty写一个爬虫脚本每天定时给女(男)朋友发微信暖心话

项目地址

https://github.com/Mcbai/WeChat-bot

使用库

基本思路

  1. 抓取 墨迹天气 和 [one·一个] 的数据
  2. 编写展示用的模板并自定义样式
  3. 处理抓取到的数据渲染模板
  4. 抓取模板页并截图
  5. 操作微信发送消息
  6. 定时处理任务

最终效果

webwxgetmsgimg

点开图片

template

目录与配置

目录结构:

image

配置代码:

image

在配置里可以修改提醒发送的时间,想要说的话等。

需要注意的坑:

puppeteer 需要安装 chromium,所以要修改 puppeteer 的下载源:

npm config set puppeteer_download_host https://npm.taobao.org/mirrors

但比较坑的是,我在云服务器上修改了源也没下载下来,所以到现在还没能放到服务器上去,只能在自己的电脑上起 node server。

后续优化:

  1. 代码结构
  2. 生成的图片质量
  3. 生成聊天记录日志,别人撤回的消息也能看见了
  4. 保存每天生成的图片

最后

希望所有猿(媛)都能遇见自己喜欢,也喜欢自己的另一半~

【译】JavaScript ES6 最值得掌握的5个特性

原文链接Top five features in JavaScript ES6 Worth Mastering

这篇文章是我在众成翻译上的最新译文

JavaScript ES6 最值得掌握的5个特性

Posted: Sept 20th, 2017

JavaScript ES6 添加了一系列新的语言特性,其中一些特性比其它更具有开创性以及更广的可用性。比如像 ES6 类 这样的特性,虽然新奇,但其实仅仅是在 JavaScript 中创建类的已有方法之上的一种语法糖。而像生成器(generator)这样的功能,虽然非常强大,但却是为了针对性的任务所保留的。

从在过去的12个月里所从事的不同 JavaScript 相关项目中,我发现有 5 个 ES6 特性是不可或缺的,因为它们真正简化了 JavaScript 普通任务的完成方式。你心中的前 5 名可能和我的不一样,如果是的话,我希望你能在结尾的评论区分享它们。

让我们正式开始!

  1. 箭头函数(Arrow Functions)

  2. Promises

  3. 异步函数(Async Functions)

  4. 解构(Destructuring)

  5. 默认和剩余参数(Default and Rest Parameters)

1) JavaScript 箭头函数

在 ES6 JavaScript 中,我最喜欢的新增特性之一并不是一个全新特性,而是一个我每次使用都能让我微笑的新语法。我说的就是箭头函数,它提供了一种极致优雅和简洁的方式来定义匿名函数。

简而言之,箭头函数就是丢掉了关键字 function,然后用一个箭头 => 来分离一个匿名函数的参数部分和函数体:

(x, y) => x * y;

这相当于:

function(x, y){
	return x * y;
}

或者:

(x, y) => {
	var factor = 5;
	var growth = (x-y) * factor;
}

完全等价于:

function(x, y){
	var factor = 5;
	var growth = (x-y) * factor;
}

在使用传统的匿名函数时,箭头函数还消除了一个关键的错误源,即函数内的 this 对象的值。使用箭头函数,this 是基于词法绑定,这仅仅是意味着它的值被绑定到父级作用域的一种奇特的方式,并且永远不会改变。如果一个箭头函数定义在一个自定义对象 countup 中,this 值毫无疑问地指向 countup。比如:

var countup = {
	counter: 15,
     
	start:function(){
		window.addEventListener('click', () => {
			alert(this.counter) // correctly alerts 15 due to lexical binding of this
		})
	}
};
 
countup.start();

对比传统匿名函数,this 的值在哪变化取决于它被定义的上下文环境。当在上面的例子中尝试引用 this.counter,结果将返回 undefined,这种行为可能会把很多不熟悉动态绑定的复杂性的人搞糊涂。使用箭头函数,this 的值总是可预测并且容易推断的。

对于箭头函数的详细讲解, 请看 "Overview of JavaScript Arrow Functions".

2) JavaScript Promises

JavaScript ES6 Promises 使异步任务的处理方式变成线性, 这是大多数现代Web应用程序中的一项任务。 而不是依靠回调函数 —— 通过JavaScript框架(如jQuery)普及。JavaScript Promises 使用一个中心直观的机制来跟踪和响应异步事件。它不仅使调试异步代码变得更容易,而且使得编写它也是一种乐趣。

所有 JavaScript Promise 都是通过 Promise() 构造函数开始和结束:

const mypromise = new Promise(function(resolve, reject){
 // 在这编写异步代码
 // 调用 resolve() 来表示任务成功完成
 // 调用 reject() 来表示任务失败
})

在内部使用 resolve()reject() 方法,当一个 Promise 被完成或拒绝时,我们可以分别向一个 Promise 对象发出信号。 then()catch() 方法随后可以被调用,用以处理完成或拒绝 Promise 后的工作。

我用下面一个被注入 XMLHttpRequest 函数的 Promise 的变种,来一个接一个的检索外部文件内容:

function getasync(url) {
	return new Promise((resolve, reject) => {
		const xhr = new XMLHttpRequest()
		xhr.open("GET", url)
		xhr.onload = () => resolve(xhr.responseText)
		xhr.onerror = () => reject(xhr.statusText)
		xhr.send()
	})
}

getasync('test.txt').then((msg) => {
	console.log(msg) // echos contents of text.txt
	return getasync('test2.txt')
}).then((msg) => {
	console.log(msg) // echos contents of text2.txt
	return getasync('test3.txt')
}).then((msg) => {
	console.log(msg) // echos contents of text3.txt
})

要掌握 JavaScript Promises 的关键点,例如 Promise 链和并行执行 Promise,请阅读 "Beginner's Guide to Promises".

3) JavaScript 异步函数

除了 JavaScript Promise,异步函数进一步重写了传统的异步代码结构,使其更具可读性。每当我向客户展示带有async 编程功能的代码时,第一个反应总是令人惊讶,随之而来的是了解它是如何工作的好奇心。

一个异步函数由两部分构成:

1) 一个以 async 为前缀的常规函数

async function fetchdata(url){
	// Do something
	// Always returns a promise
}

2) 在异步函数(Async function)内,使用 await 关键字调用异步操作函数

一个例子胜过千言万语。下面是基于上面示例重写的 Promise,以便使用 Async functions代替:

function getasync(url) { // same as original function
	return new Promise((resolve, reject) => {
		const xhr = new XMLHttpRequest()
		xhr.open("GET", url)
		xhr.onload = () => resolve(xhr.responseText)
		xhr.onerror = () => reject(xhr.statusText)
		xhr.send()
	})
}

async function fetchdata(){ // main Async function
	var text1 = await getasync('test.txt')
	console.log(text1)
	var text2 = await getasync('test2.txt')
	console.log(text2)
	var text3 = await getasync('test3.txt')
	console.log(text3)
	return "Finished"
}

fetchdata().then((msg) =>{
	console.log(msg) // logs "finished"
})

上面的例子运行时会输出“test.txt”,“test2.txt”,“test3.txt”,最后是“Finished”。

如你所见,在异步函数中,我们把异步函数 getasync() 当作是同步函数调用 - 没有 then() 方法或回调函数通知进行下一步。无论何时遇到关键字 await,执行都会暂停,直到 getasync() 解决,然后再转到异步函数中的下一行。结果与纯粹的基于 Promise,使用一串 then 方法的方式一样。

要掌握异步函数,包括如何 await 并行执行函数,请阅读 "Introduction to JavaScript Async Functions- Promises simplified"

4) JavaScript 解构

除了箭头函数,这是我每天使用最多的 ES6 功能。ES6 解构并非一个新功能,而是一个新的赋值语法,可以让您快速解压缩对象属性和数组中的值,并将它们分配给各个变量。

var profile = {name:'George', age:39, hobby:'Tennis'}
var {name, hobby} = profile // destructure profile object
console.log(name) // "George"
console.log(hobby) // "Tennis"

这里我用解构快速提取 profile 对象的 namehobby 属性 。

使用别名,你可以使用与你正在提取值的对象属性不同的变量名:

var profile = {name:'George', age:39, hobby:'Tennis'}
var {name:n, hobby:h} = profile // destructure profile object
console.log(n) // "George"
console.log(h) // "Tennis"

嵌套对象解构

解构也可以与嵌套对象一起工作,我一直使用它来快速解开来自复杂的JSON请求的值:

var jsondata = {
	title: 'Top 5 JavaScript ES6 Features',
	Details: {
		date: {
			created: '2017/09/19',
			modified: '2017/09/20',
		},
		Category: 'JavaScript',
	},
	url: '/top-5-es6-features/'
};

var {title, Details: {date: {created, modified}}} = jsondata
console.log(title) // 'Top 5 JavaScript ES6 Features'
console.log(created) // '2017/09/19'
console.log(modified) // '2017/09/20'

解构数组

数组的解构与在对象上的工作方式类似,除了左边的花括号使用方括号代替:

var soccerteam = ['George', 'Dennis', 'Sandy']
var [a, b] = soccerteam // destructure soccerteam array
console.log(a) // "George"
console.log(b) // "Dennis"

你可以跳过某些数组元素,通过使用逗号(,):

var soccerteam = ['George', 'Dennis', 'Sandy']
var [a,,b] = soccerteam // destructure soccerteam array
console.log(a) // "George"
console.log(b) // "Sandy"

对我而言,解构消除了传统方式提取和分配对象属性和数组值的所有摩擦。要充分掌握ES6解构的复杂性和潜力,请阅读"Getting to Grips with ES6: Destructuring".

5) 默认和剩余参数(Default and Rest Parameters)

最后,我最想提出的ES6的两个特性是处理函数参数。几乎我们在JavaScript中创建的每个函数都接受用户数据,所以这两个特性在一个月中不止一次地派上用场。

默认参数(Default Parameters)

我们都使用过一下模式来创建具有默认值的参数:

function getarea(w,h){
  var w = w || 10
  var h = h || 15
  return w * h
}

有了ES6对默认参数的支持,显式定义的参数值的日子已经结束:

function getarea(w=10, h=15){
  return w * h
}
getarea(5) // returns 75

关于 ES6 默认参数的更多详情 在这.

剩余参数(Rest Parameters)

ES6中的 Rest Parameters 使得将函数参数转换成数组的操作变得简单。

function addit(...theNumbers){
  // get the sum of the array elements
	return theNumbers.reduce((prevnum, curnum) => prevnum + curnum, 0) 
}

addit(1,2,3,4) // returns 10

通过在命名参数前添加3个点 ...,在该位置和之后输入到函数中的参数将自动转换为数组。

没有 Rest Parameters, 我们不得不做一些复杂的操作比如 手动将参数转换为数组

function addit(theNumbers){
    // force arguments object into array
	var numArray = Array.prototype.slice.call(arguments) 
	return numArray.reduce((prevnum, curnum) => prevnum + curnum, 0)
}

addit(1,2,3,4) // returns 10

Rest parameters 只能应用于函数的参数的一个子集,就像下面这样,它只会将参数从第二个开始转换为数组:

function f1(date, ...lucknumbers){
	return 'The Lucky Numbers for ' + date + ' are: ' + lucknumbers.join(', ')
}

alert( f1('2017/09/29', 3, 32, 43, 52) ) // alerts "The Lucky Numbers for 2017/09/29 are 3,32,43,52"

对于 ES6 中 Rest Parameters 完整规范,看这里.

结论

你同意我所说的 ES6 特性的前五名吗?哪个是你最常用的,请在评论区和大家分享。

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.