Web前端

前端,技术,生活的艺术


  • Startseite

  • Über

  • Tags

  • Kategorien

  • Archiv

Vue中computed计算属性原理

Veröffentlicht am 2018-11-30 |

Computed 计算属性是 Vue 中常用的一个功能,但你理解它是怎么工作的吗?

官网的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<div id="example">
<p>Original message: "{{ message }}"</p>
<p>Computed reversed message: "{{ reversedMessage }}"</p>
</div>

var vm = new Vue({
el: '#example',
data: {
message: 'Hello'
},
computed: {
// a computed getter
reversedMessage: function () {
// `this` points to the vm instance
return this.message.split('').reverse().join('')
}
}
})

Situation

Vue 里的 Computed 属性非常频繁的被使用到,但并不是很清楚它的实现原理。比如:计算属性如何与属性建立依赖关系?属性发生变化又如何通知到计算属性重新计算?

关于如何建立依赖关系,我的第一个想到的就是语法解析,但这样太浪费性能,因此排除,第二个想到的就是利用 JavaScript 单线程的原理和 Vue 的 Getter 设计,通过一个简单的发布订阅,就可以在一次计算属性求值的过程中收集到相关依赖。

因此接下来的任务就是从 Vue 源码一步步分析 Computed 的实现原理。

Task

分析依赖收集实现原理,分析动态计算实现原理。

Action

data 属性初始化 getter setter

(Obeject.defineProperty是Object的一个方法,第一个参数是对象名称,第二个参数是要设置的属性名,第三个参数是一个对象,它可以设置这个属性是否可修改、可写等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
// 判断是否处于依赖收集状态
if (Dep.target) {
// 建立依赖关系
dep.depend()
...
}
return value
},
set: function reactiveSetter (newVal) {
...
// 依赖发生变化,通知到计算属性重新计算
dep.notify()
}
})

computed 计算属性初始化

// 初始化计算属性
function initComputed (vm: Component, computed: Object) {
    ...
    // 遍历 computed 计算属性
    for (const key in computed) {
        ...
        // 创建 Watcher 实例
        // create internal watcher for the computed property.
        watchers[key] = new Watcher(vm, getter || noop, noop, computedWatcherOptions)

        // 创建属性 vm.reversedMessage,并将提供的函数将用作属性 vm.reversedMessage 的 getter,
        // 最终 computed 与 data 会一起混合到 vm 下,所以当 computed 与 data 存在重名属性时会抛出警告
        defineComputed(vm, key, userDef)
        ...
    }
}

export function defineComputed (target: any, key: string, userDef: Object | Function) {
    ...
    // 创建 get set 方法
    sharedPropertyDefinition.get = createComputedGetter(key)
    sharedPropertyDefinition.set = noop
    ...
    // 创建属性 vm.reversedMessage,并初始化 getter setter
    Object.defineProperty(target, key, sharedPropertyDefinition)
    }

    function createComputedGetter (key) {
    return function computedGetter () {
        const watcher = this._computedWatchers && this._computedWatchers[key]
        if (watcher) {
        if (watcher.dirty) {
            // watcher 暴露 evaluate 方法用于取值操作
            watcher.evaluate()
        }
        // 同第1步,判断是否处于依赖收集状态
        if (Dep.target) {
            watcher.depend()
        }
        return watcher.value
        }
    }
}

nodejs 模块

Veröffentlicht am 2018-10-10 |

对nodeJS早有耳闻,但是一直迟迟没有对它下手,虽然在项目中有用过nodejs,但也没有系统的学习过,这次我们就来探究一下。

node.js是什么东西?

  • nodejs是以chrome v8为运行环境的一个平台,它不是一门语言,而是一个平台
  • nodejs致力于是构建速度快,稳定的网络程序更简单
  • 它具有事件驱动和非阻塞I/O的特色,使之轻量级并且高效率
  • 它非常适合在分布式设备运行数据密集型实时应用程序

讲到这里,我们大概的明白了nodejs是一个平台,是一个环境,它是由chrome v8引擎来做底层支持,使用JavaScript来做语言支持,大概意思就是我们之前使用JavaScript运行在浏览器端去处理dom,bom操作等等,现在JavaScript运行在服务器去处理数据的增删改查,接受请求,发送数据,查找修改文件。
JavaScript以前是在浏览器端跑,现在有了nodejs就可以在后端跑,就像汽车原本只能在公路上跑,现在你给他装了nodejs,它现在可以在水里跑了一样.

安装开发环境

1.官网下载nodeJS安装包。
2.在下载完安装包后,按照默认程序安装。
3.检验是否安装成功,打开命令提示符,输入node – v,如果正常就会出现版本号的输出。
在安装nodejs时,也一并安装了npm, 命令行输入npm -v 查看npm版本。
这样环境就已经搭建完成,很简单。
接下来我们就来了解一下nodejs的基础知识

模块

Node应用是由模块组成的,Node遵循了CommonJS的模块规范,来隔离每个模块的作用域,使每个模块在它自身的命名空间中执行
Node.js 提供了 exports 和 require 两个对象,其中 exports 是模块公开的接口,require 用于从外部获取一个模块的接口,即所获取模块的 exports 对象。
创建一个模块

1
2
3
4
5
6
7
8
9
10
11
//hello.js 
function Hello() {
var name;
this.setName = function(thyName) {
name = thyName;
};
this.sayHello = function() {
console.log('Hello ' + name);
};
};
module.exports = Hello; // 暴露出去,其他文件可以引入这个模块

引入这个模块

1
2
3
4
5
//main.js 
var Hello = require('./hello'); // 引入hello文件
hello = new Hello();
hello.setName('BYVoid');
hello.sayHello();

这是我们自己写的一个简单的模块,nodejs还提供了一些原生的模块,如http、fs、path等,都是可以直接引入使用的,这些我们后面再介绍。

fs 处理文件的模块

fs模块用于对系统文件及目录进行读写操作

理解同步和异步方法

模块中所有方法都有同步和异步两种形式(异步方式主要是使用回调函数) , 例如读取文件内容的函数有异步的 fs.readFile() 和同步的 fs.readFileSync()。异步的方法函数最后一个参数为回调函数,回调函数的第一个参数包含了错误信息(error)。
建议使用异步方法,比起同步,异步方法性能更高,速度更快,而且没有阻塞

readFile读取文件

fs.readFile(filename,[option],callback) 方法读取文件。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
var fs = require('fs'); // 引入fs模块
// 异步
fs.readFile('./1.txt', 'utf-8', function(err, data) {
// 读取文件失败/错误
if (err) {
throw err;
}
// 读取文件成功
console.log('utf-8: ', data);
});

// 同步
fs.readFileSync('./1.txt','utf-8')。

WriteFile写入文件

使用fs.writeFile(filename,data,[options],callback)写入内容到文件。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var fs = require('fs'); // 引入fs模块

// 写入文件内容(如果文件不存在会创建一个文件)
//配置参数flag:
/*
* a :追加
* w :写入
* r :读取
* */
fs.writeFile('./2.txt', 'hello', { 'flag': 'a' }, function(err) {
if (err) {
throw err;
}
console.log('成功');
});

// 同步只是去掉回调函数

文件删除

1
2
3
4
5
6
7
8
9
10
// 异步
fs.unlink("2.txt",function (err) {
if(err){
return console.log(err)
}else {
console.log("删除成功")
}
})

// 同步只是去掉回调函数

目录操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 创建 new 目录
fs.mkdir('./new', function(err) {
if(err){
return console.log(err)
}else {
console.log('success.');
}
});

// 删除目录 ,只能删除空文件夹
fs.rmdir("./new",function (err) {
if(err){
return console.log(err)
}else {
console.log("删除成功");
}
})

http模块

对于NodeJS来说比较重要的一块就是http模块,我们使用http模块可以自己搭建服务器,从而监听客户端的一些请求,和客户端合作接受请求并且返回数据。

http模块搭建简易服务器

1
2
3
4
5
6
7
8
9
10
11
var http = require('http');

http.createServer(function(req,res){
res.writeHead(200,{
"content-type":"text/plain"
});
res.write("hello nodejs");
res.end();
}).listen(3000);

console.log('Server is running at http://127.0.0.1:3000/');

这就是一个简易的服务器,当然我们的业务场景肯定不会这么简单,下面继续扩展一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
'use strict';

// 引入一些需要用到的模块
var
fs = require('fs'),
url = require('url'),
path = require('path'),
http = require('http');

// 从命令行参数获取root目录,默认是当前目录:
var root = path.resolve(process.argv[2] || '.');

console.log('Static root dir: ' + root);

// 创建服务器:
var server = http.createServer(function (request, response) {
// 获得URL的path,类似 '/css/test.css':
var pathname = url.parse(request.url).pathname;
// 获得对应的本地文件路径,类似 '/src/www/css/test.css':
var filepath = path.join(root, pathname);
// 获取文件状态:
fs.stat(filepath, function (err, stats) {
if (!err && stats.isFile()) {
// 没有出错并且文件存在:
console.log('200 ' + request.url);
// 发送200响应:
response.writeHead(200);
// 将文件流导向response:
fs.createReadStream(filepath).pipe(response);
} else {
// 出错了或者文件不存在:
console.log('404 ' + request.url);
// 发送404响应:
response.writeHead(404);
response.end('404 Not Found');
}
});
});

server.listen(3000);

console.log('Server is running at http://127.0.0.1:3000/');

pipe()管道方法自动读取文件内容并输出到HTTP响应

之后再来介绍里面具体的方法

axios拦截器

Veröffentlicht am 2018-10-08 |

页面发送http请求,很多情况我们要对请求和其响应进行特定的处理;如果请求数非常多,单独对每一个请求进行处理会变得非常麻烦,程序的优雅性也会大打折扣。好在强大的axios为开发者提供了这样一个API:拦截器。拦截器分为 请求(request)拦截器和 响应(response)拦截器。

axios拦截器简单介绍

请求拦截器

1
2
3
4
5
6
7
axios.interceptors.request.use(function (config) {
// 在发起请求请做一些业务处理
return config;
}, function (error) {
// 对请求失败做处理
return Promise.reject(error);
});

响应拦截器

1
2
3
4
5
6
7
axios.interceptors.response.use(function (response) {
// 对响应数据做处理
return response;
}, function (error) {
// 对响应错误做处理
return Promise.reject(error);
});

vue添加axios拦截器

安装 axios

npm install axios –save-dev

新建文件 axios.js

开始统一封装axios, 首先引入axios、qs依赖

1
2
import axios from "axios";
import qs from "qs";

然后创建一个axios实例,这个process.env.BASE_URL在config/dev.evn.js、prod.evn.js里面进行配置:

1
2
3
4
5
/****** 创建axios实例 ******/
const service = axios.create({
baseURL: process.env.BASE_URL, // api的base_url
timeout: 5000 // 请求超时时间
});

使用request拦截器对axios请求配置做统一处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
service.interceptors.request.use(config => {    
app.$vux.loading.show({
text: '数据加载中……'
});
config.method === 'post'
? config.data = qs.stringify({...config.data})
: config.params = {...config.params};
config.headers['Content-Type'] = 'application/x-www-form-urlencoded';
return config;
}, error => { //请求错误处理
app.$vux.toast.show({
type: 'warn',
text: error
});
Promise.reject(error)
}
);

对response做统一处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
service.interceptors.response.use(    
response => { //成功请求到数据
app.$vux.loading.hide();
//这里根据后端提供的数据进行对应的处理
if (response.data.result === 'TRUE') {
return response.data;
} else {
app.$vux.toast.show({
//常规错误处理
type: 'warn',
text: response.data.data.msg
});
}
},
error => { //响应错误处理console.log('error');
console.log(error);
console.log(JSON.stringify(error));
let text = JSON.parse(JSON.stringify(error)).response.status === 404
? '404'
: '网络异常,请重试';
app.$vux.toast.show({
type: 'warn',
text: text
});
return Promise.reject(error)
}
)

将axios实例暴露出去

1
export default service;

这样一个简单的拦截器就完成了

在main.js中进行引用,并配置一个别名($ajax)来进行调用

1
2
3
4
import axios from 'axios'
import '../axios.js' //axios.js的路径

Vue.prototype.$ajax = axios

应用:一个简单的登录接口

1
2
3
4
5
6
7
8
9
10
this.$ajax({
  method: 'post',
  url: '/login',
  data: {
    'userName': 'haha',
    'password': '123456'
  }
}).then(res => {
  console.log(res)
})

使用场景

eg: axios拦截器对路由进行拦截

1.路由拦截

在定义路由的时候就需要多添加一个自定义字段requireAuth,用于判断该路由的访问是否需要登录。如果用户已经登录,则顺利进入路由,否则就进入登录页面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const routes = [
{
path: '/',
name: '/',
component: Index
},
{
path: '/repository',
name: 'repository',
meta: {
requireAuth: true, // 添加该字段,表示进入这个路由是需要登录的
},
component: Repository
},
{
path: '/login',
name: 'login',
component: Login
}
];

定义完路由后,我们主要是利用vue-router提供的钩子函数beforeEach()对路由进行判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
router.beforeEach((to, from, next) => {
if (to.meta.requireAuth) { // 判断该路由是否需要登录权限
if (token) { // 判断当前的token是否存在
next();
}
else {
next({
path: '/login',
query: {redirect: to.fullPath} // 将跳转的路由path作为参数,登录成功后跳转到该路由
})
}
}
else {
next();
}
})

to.meta中是我们自定义的数据,其中就包括我们刚刚定义的requireAuth字段
通过这个字段来判断该路由是否需要登录权限
需要的话,同时当前应用不存在token,则跳转到登录页面,进行登录。登录成功后跳转到目标路由。

这种方式只是简单的前端路由控制,并不能阻止用户访问,假设有一种情况:当前token失效了,但是token依然保存在本地。这时候你去访问需要登录权限的路由时,实际上应该让用户重新登录。这时候就需要结合 http 拦截器 + 后端接口返回的http 状态码来判断。

2.拦截器

要想统一处理所有http请求和响应,就得用上 axios 的拦截器。通过配置http response inteceptor,当后端接口返回401 Unauthorized(未授权),让用户重新登录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// http request 拦截器
axios.interceptors.request.use(
config => {
if (stoken) { // 判断是否存在token,如果存在的话,则每个http header都加上token
config.headers.Authorization = `token ${store.state.token}`;
}
return config;
},
err => {
return Promise.reject(err);
});

// http response 拦截器
axios.interceptors.response.use(
response => {
return response;
},
error => {
if (error.response) {
switch (error.response.status) {
case 401:
// 返回 401 清除token信息并跳转到登录页面

router.replace({
path: 'login',
query: {redirect: router.currentRoute.fullPath}
})
}
}
return Promise.reject(error.response.data) // 返回接口返回的错误信息
});

es6中class的深入了解

Veröffentlicht am 2018-08-27 |

相信es6中的class(类)对我们来说并不陌生,在我的工作中,也用到了ES6这一特性,虽然一直在使用,但是并没有去认真了解过它,于是做了一些功课,在这里跟大家分享自己的一些心得和看法。

比较

在ES6以前,JS并没有给我们提供对类的支持,常用做法是用构造函数来模拟类的实现,通过将属性和方法定义在原型上将自身的属性共享给它的实例。
来看一个例子:

1
2
3
4
5
6
7
8
9
10
11
function Point(x, y) {
this.x = x;
this.y = y;
}

Point.prototype.toString = function () {
return '(' + this.x + ', ' + this.y + ')';
};

let p = new Point(1, 2);
p.toString()

ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念。通过class关键字,可以定义类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//定义类
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}

toString() {
return `( ${this.x}, ${this.y} )`;
}
}

let p = new Point(1,2)
p.toString()
//(1,2)

解释:
constructor:构造方法,
this: 表示实例对象,
toString: 实例的方法

constructor

constructor是类的默认方法,通过new 命令生成对象实例时,自动调用该方法,一个类必须有constructor方法,如果没有显示定义,会默认添加一个空的constructor方法

1
2
3
4
class Point{ } //等同于
class Point {
constructor () { }
}

constructor方法默认返回实例对象(this),如果我们指定返回另一个对象

1
2
3
4
5
6
7
8
class Person { 
constructor () {
return Object.create(null);
}
}
let p = new Person()
p instanceof Person //false
// 实例 instanceof 构造函数 用来判断实例是否是构造函数的实例

ES6 的(class)类,完全可以看作构造函数的另一种写法, 类的数据类型就是函数,类本身就指向构造函数

1
2
typeof Point // "function"
Point === Point.prototype.constructor // true

类必须使用new调用,否则会报错。
在定义类的时候,前面不需要加function关键字

this 指向

如果类的方法内部含有this,它默认指向类的实例。但是,必须非常小心,一旦单独使用该方法,很可能报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Logger {
printName(name = 'there') {
this.print(`Hello ${name}`);
}

print(text) {
console.log(text);
}
}

const logger = new Logger();
const { printName } = logger;
printName(); // TypeError: Cannot read property 'print' of undefined

类的实例对象

与 ES5 一样,实例的属性除非显式定义在其本身(即定义在this对象上),否则都是定义在原型上(即定义在class上)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//定义类
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return `( ${this.x}, ${this.y} )`;
}
}

let point = new Point(2, 3);

point.toString() // (2, 3)

point.hasOwnProperty('x')
point.hasOwnProperty('y')
point.hasOwnProperty('toString')
point.__proto__.hasOwnProperty('toString')

Class 的静态方法

所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。

1
2
3
4
5
6
7
8
class Foo { 
static classMethod() {
return 'hello';
}
}
Foo.classMethod() // 'hello'
let foo = new Foo();
foo.classMethod() // TypeError: foo.classMethod is not a function

思考一下

1
2
3
4
5
6
7
8
9
10
11
12
13
class Foo {
static bar () {
this.baz();
}
static baz () {
console.log('hello');
}
baz () {
console.log('world');
}
}

Foo.bar() // ??

如果静态方法包含this关键字,这个this指的是类,而不是实例, 从这个例子还可以看出,静态方法可以与非静态方法重名。
所以这个例子最终打印出来的会是 hello.

Class 的静态属性和实例属性

静态属性

静态属性和静态方法都是一样,都是定义在类上,而不是实例对象上。
先来感受一下:

1
2
3
4
class Foo {
static prop = 1;
}
Foo.prop // 1

类的实例属性

eg:

1
2
3
4
5
6
7
class MyClass {
myProp = 42;

constructor() {
console.log(this.myProp); // 42
}
}

以前我们定义实例属性,只能写在constructor方法里面,像下面这样

1
2
3
4
5
6
7
8
class Index extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
}

类的继承

Class 可以通过extends关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 父类
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
static hello() {
console.log('hello world');
}
}

// 子类
class ColorPoint extends Point {
constructor(x, y, color) {
this.color = color; // ReferenceError
super(x, y); // 调用父类的constructor(x, y)
this.color = color; // 正确
}
}

子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super方法,子类就得不到this对象。
父类的静态方法,也会被子类继承

类的私有属性和私有方法

私有属性

ES6 不支持私有属性。目前,有一个提案,为class加了私有属性。方法是在属性名之前,使用#表示
之所以要引入一个新的前缀#表示私有属性,是因为js没有private关键字
另外,Ruby 语言使用@表示私有属性,ES6 没有用@符号而使用#,是因为@已经被留给了 Decorator

1
2
3
4
5
6
7
8
class Point {
#x;
constructor(x = 0) {
#x = +x; // 写成 this.#x 亦可
}
get x() { return #x }
set x(value) { #x = +value }
}

私有方法

私有方法是常见需求,但 ES6 不提供,只能通过其他方法模拟实现
第一种方法:

1
2
3
4
5
6
7
8
9
10
11
class Widget {
// 公有方法
foo (baz) {
this._bar(baz);
}
// 私有方法
_bar(baz) {
return this.snaf = baz;
}
// ...
}

第二种方法
还有一种方法是利用Symbol值的唯一性,将私有方法的名字命名为一个Symbol值

1
2
3
4
5
6
7
8
9
10
11
12
13
const bar = Symbol('bar');
const snaf = Symbol('snaf');
export default class myClass{
// 公有方法
foo(baz) {
this[bar](baz);
}
// 私有方法
[bar](baz) {
return this[snaf] = baz;
}
// ...
};

web图片加载与渲染时机

Veröffentlicht am 2018-08-05 |

前言

管理好网页的图片资源,可以避免不必要的流量和提高用户体验。

浏览器的工作流程

要研究图片资源的加载,就不得不先熟悉浏览器的工作原理,下面这张图可以很清晰的说明
image

从上图可看出,浏览器加载一个HTML页面后进行如下操作(详细解释图片资源渲染):

  • 解析HTML(遇到img标签加载图片) —> 构建DOM树
  • 加载css —> 解析css(遇到背景图片链接不加载) —> 构建css树
  • 加载js代码 —> 执行js代码
  • 把DOM树和css树匹配构建渲染树(遍历DOM树时加载对应样式规则上的背景图片)
  • 计算元素位置进行布局
  • 绘制(开始渲染图片)

图片加载与渲染规则

了解了浏览器的工作流程,现在来说说图片的加载和渲染,并不是所有的img标签图片和样式表背景图片都会加载

重复图片

1
2
3
4
5
6
.img-bg {
background: url(./demo.png);
}
<div class="img-bg"></div>
<img src="./demo.png">
<img src="./demo.png">

页面中多个img标签或样式表中的背景图片图片路径是同一个,图片只加载一次。
原因:浏览器请求资源时,都会先判断是否有缓存,若有缓存且未过期则会从缓存中读取,不会再次请求。先加载的图片会存储到浏览器缓存中,后面再次请求同路径图片时会直接读取缓存中的图片。

设置display: none的图片

1
<img src="./demo1.png" style="display: none;">

设置了display:none属性的元素,图片不会渲染出来,但会加载。
原因:在解析HTML时,遇到img标签就加载图片,DOM树和样式规则树匹配构建渲染树时,只会把可见元素和它对应的样式规则结合一起产出到渲染树

1
2
3
4
5
6
7
8
9
<style>
.img {
background: url(./demo.png);
}
</style>
<div style="display:none">
<img src="./demo1.png">
<div class="img"></div>
</div>

如果父元素设置了display:none属性,那么子元素样式表中的背景图片不会渲染出来,也不会加载;而img标签的图片不会渲染出来,但会加载。
原因:在解析HTML时,遇到img标签就加载图片,当构建渲染树遇到了设置了display:none属性的不可见元素时,不会继续遍历不可见元素的子元素,因此不会加载该元素中子元素的背景图片

伪类的背景图片

1
2
3
4
5
6
7
.img {
background: url(./domo.png);
}
.img:hover{
background: url(./demo1.png);
}
<div class="img"></div>

当触发伪类的时候,伪类样式上的背景图片才会加载。
原因:触发hover前,构建渲染树过程中,遍历DOM树时,该元素匹配的样式规则是无hover状态选择器.img的样式;触发hover后,因为.img:hover的优先级比较高,构建新的渲染树过程中,会加载伪类的背景图片。

不存在元素的背景图片

1
2
3
4
.img111 {
background: url(./domo.png);
}
<div class="img"></div>

不存在元素的背景图片不会加载。
原因:不存在的元素不会产出到DOM树上,构建渲染树过程中遍历DOM树时无法遍历不存在的元素,因此不会加载图片,也不会产出到渲染树上。

应用场景

占位图

当使用样式表中的背景图片作为占位符时,要把背景图片转为base64格式。这是因为背景图片加载的顺序在img标签后面,背景图片可能会在img标签图片加载完成后才开始加载,达不到想要的效果。

预加载

  • 使用上文讲到的,设置了display:none属性的元素,图片不会渲染出来,但会加载。把要预加载的图片加到设置了display:none的元素背景图或img标签里。
  • 在javascript创建img对象,把图片url设置到img对象的src属性里。

js排序算法

Veröffentlicht am 2018-06-21 |

1.冒泡排序

原理

依次比较相邻的两个值,如果后面的比前面的小,则将小的元素排到前面。依照这个规则进行多次并且递减的迭代,直到顺序正确。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var examplearr=[4,24,15,37,55,71,28,30];
function sort(arr){
for(i=0;i<arr.length-1;i++){
for(j=0;j<arr.length-1-i;j++){
if(arr[j]>arr[j+1]){
var temp=arr[j];
arr[j]=arr[j+1];
arr[j+1]=temp;
}
}
}
return arr;
}
sort(examplearr);
console.log(examplearr);

解析

使用两个循环

当i=0的时候,执行内层循环,从j=0执行到j=6,这也就是第一遍排序,结果是将最大的数排到了最后。
当i=1的时候,再次执行内层循环,由于最大的数已经在最后了,没有必要去比较数组的最后两项,这也是为什么要用j<arr.length-1-i

按照这个规律,每次将剩下数组里面最大的一个数排到最后面,当执行到i=6时,就只需要比较第一项和第二项,交换之后就排序结束。

算法分析

最佳情况:T(n) = O(n)
最差情况:T(n) = O(n2)
平均情况:T(n) = O(n2)

2.选择排序

原理

选择排序算法是一种原址比较算法。选择排序的大致思路是找到数据结构中的最小值,并将其放置在第一位,接着找到第二小的值放到第二位,以此类推。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function selectSort(arr){
var len=arr.length;
var temp;
for(var i=0;i<len-1;i++){
for(var j=i+1;j<len;j++){
if(arr[j]<arr[i]){
temp=arr[j];
arr[j]=arr[i];
arr[i]=temp;
}
}
i++;
}
return arr;
}

解析

把每一个数都与第一个数比较,如果小于第一个数,就把它们交换位置;这样一轮下来,最小的数就排到了最前面;重复n-1轮,就实现了选择排序

选择排序和冒泡排序思想上有些相近

算法分析

最佳情况:T(n) = O(n2)
最差情况:T(n) = O(n2)
平均情况:T(n) = O(n2)

3.插入排序

插入排序每次排一个数组项,以此方式构建最后的排序数组,假定第一项已经排序了,接着,它和第二项进行比较,第二项是应该待在原位还是插到第一项之前呢?这样头两项就已经正确排序,接着和第三项比较(它是该插入第一、第二还是第三的位置呢?)

1
2
3
4
5
6
7
8
9
10
11
12
13
this.insertionSort = function(){
var length = array.length,
j, temp;
for (var i=1; i<length; i++){
j = i;
temp = array[i];
while (j>0 && array[j-1] > temp{
array[j] = array[j-1];
j--;
}
array[j] = temp;
}
};

排序过程大概如下:

从第一个元素开始,该元素可以认为已经被排序;
取出下一个元素,在已经排序的元素序列中从后向前扫描;
如果该元素(已排序)大于新元素,将该元素移到下一位置;
重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
将新元素插入到该位置后;
重复步骤2~5。

算法分析

最佳情况:输入数组按升序排列。T(n) = O(n)
最坏情况:输入数组按降序排列。T(n) = O(n2)
平均情况:T(n) = O(n2)

未完待续。。。

vue 组件的通信方式

Veröffentlicht am 2018-05-21 |

前言

在使用Vue开发的时候,组件之间的通信很常用,每次在需要用到的时候,都要去查文档,所以干脆整理一下,免得总是忘记。

父组件向子组件传递数据

1.props:这个是最简单的一种通信了,通过props就可以向子组件传递数据了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**父组件代码**/
<template>
<Child :title-txt="showTitleTxt"></Child>
</template>
<script>
import Child from './child'
export default {
name: 'index',
components: {
Child
},
data () {
return {
showTitleTxt: '这是父组件'
}
}
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
/**子组件代码**/
<template>
<header>
{{titleTxt}}
</header>
</template>
<script>
export default {
name: 'header-box',
props: ['titleTxt']
}
</script>

2.使用$children可以在父组件中访问子组件。

子组件向父组件传递数据

子组件向父组件传递数据,可以有两种方式
1.props:通过props中的Object类型参数传输数据,可以通过子组件改变父组件的数据内容。这种方式是可行的,但是官方不推荐使用,因为官方定义prop是单向绑定
2.通过$on和$emit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**父组件代码**/
<template>
<Child @addFun="add"></Child>
{{ total }}
</template>

<script>

import Child from './child.vue';

export default {
components: {
Child
},
data() {
return {
total: 0
}
}
methods: {
add (counter) {
this.total = this.total + counter
}
}
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**子组件代码**/
<template>
<button @click="handleClick">加</button>
</template>

<script>
export default {
methods () {
data() {
return {
counter: 10
}
}
handleClick () {
this.$emit(addFun, this.counter);
}
}
}
</script>

非父子组件通信

中央事件总线

简单情况下我们可以通过使用一个空的Vue实例作为中央事件总线

1
2
3
/**bus.js**/
let bus = new Vue()
Vue.prototype.bus = bus

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**header组件**/
<template>
<header @click="changeTitle">{{title}}</header>
</template>
<script>
export default {
name: 'header',
data () {
return {
title: '头部'
}
},
methods: {
changeTitle () {
this.bus.$emit('toChangeTitle','首页')
}
}
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**footer组件**/
<template>
<footer>{{txt}}</footer>
</template>
<script>
export default {
name: 'footer',
mounted () {
this.bus.$on('toChangeTitle', (title) => {
console.log(title)
})
},
data () {
return {
txt: '尾部'
}
}
}

vuex

如果项目结构复杂化以后,这样的自定义事件变多以后代码难以管理,所以还是建议使用vuex。
虽然暂时还没有用上vuex,不见得以后用不上,可以先学习下
如果已经了解过redux或者flux,那么vuex也是类似的,官方的说法就是Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。
vuex有四个核心概念,其中state和getters主要是用于数据的存储与输出,而mutations和actions是用于提交事件并修改state中的数据。
image

参考官方的目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
├── index.html
├── main.js
├── api
│ └── ... # 抽取出API请求
├── components
│ ├── App.vue
│ └── ...
└── store
├── index.js # 我们组装模块并导出 store 的地方
├── actions.js # 根级别的 action
├── mutations.js # 根级别的 mutation
└── modules
├── dialog_store.js # 消息提示模块

index.js
1
2
3
4
5
6
7
8
9
10
11
12
/**index.js**/
import Vue from 'vue'
import vuex from 'vuex'
Vue.use(vuex);

import dialog_store from '../components/dialog_store.js';//引入某个store对象

export default new vuex.Store({
modules: {
dialog: dialog_store
}
})
1
2
3
4
5
6
/**dialog_store.js**/
export default {
state:{
show:false
}
}
1
2
3
4
5
6
7
8
9
10
/**在主入口文件中**/
import store from './store'

new Vue({
el: '#app',
router,
store,//使用store
template: '<App/>',
components: { App }
})
mutations
1
2
3
4
5
6
7
8
9
10
11
export default {
state:{//state
show:false
},
mutations:{
switch_dialog(state){//这里的state对应着上面这个state
state.show = state.show ? false : true;
//你还可以在这里执行其他的操作改变state
}
}
}
actions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 export default {
state:{//state
show:false
},
mutations:{
switch_dialog(state){//这里的state对应着上面这个state
state.show = state.show?false:true;
//你还可以在这里执行其他的操作改变state
}
},
actions:{
switch_dialog(context){//这里的context和我们使用的$store拥有相同的对象和方法
context.commit('switch_dialog');
//你还可以在这里触发其他的mutations方法
},
}
}

对于vuex,我也了解的不是很多,在具体组件中的使用,可以参考官网的购物车例子

Async 函数

Veröffentlicht am 2018-04-22 |

前言

随着 Node 7 的发布,越来越多的人开始研究据说是异步编程终级解决方案的 async/await,在我们的很多项目中都已经开始使用async函数,下面就来一起理解下Javascript处理异步的async函数。

初识async/await

Async/await 是Javascript编写异步程序的新方法。以往的异步方法无外乎回调函数和Promise。但是Async/await建立于Promise之上,async 函数算是一个语法糖,使异步函数、回调函数在语法上看上去更像同步函数,很多人认为它是异步操作的终极解决方案。

async/await语法

async

async函数返回一个 Promise 对象,可以使用then方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
async function test () {
return 'hello async'
}

const result = test();
console.log(result);

// 可以看到,控制台输出了一个Promise对象
// Promise { 'hello async' }

// 可以then() 链来处理这个 Promise 对象
test().then(v => {
console.log(v); // 输出 hello async
});

在没有 await 的情况下执行 async 函数,它会立即执行,返回一个 Promise 对象,并且,绝不会阻塞后面的语句。这和普通返回 Promise 对象的函数一样。所以这个await函数就至关重要了。

await

正常情况下,await命令后面是一个 Promise 对象。如果不是,会被转成一个立即resolve的 Promise 对象。

1
2
3
4
5
6
async function testAwait() {
return await 123;
}

testAwait().then(v => console.log(v))
// 123

只要一个await语句后面的 Promise 变为reject,那么整个async函数都会中断执行.

1
2
3
4
async function testAwait() {
await Promise.reject('出错了');
await Promise.resolve('hello'); // 不会执行
}

所以当一个 async 函数中有多个 await命令时,如果不想因为一个出错而导致其与的都无法执行,应将await放在try…catch语句中执行

1
2
3
4
5
6
7
8
9
10
11
async function testAwait {
try {
await Promise.reject('出错了');
} catch(e) {
}
return await Promise.resolve('hello world');
}

testAwait()
.then(v => console.log(v))
// hello world

另一种方法是await后面的 Promise 对象再跟一个catch方法,处理前面可能出现的错误。

1
2
3
4
5
6
7
8
9
10
async function testAwait() {
await Promise.reject('出错了')
.catch(e => console.log(e));
return await Promise.resolve('hello world');
}

testAwait()
.then(v => console.log(v))
// 出错了
// hello world

并发执行 await 命令

当一个 async 函数中有多个await时,这些 await是继发执行的,只有当前一个await后面的方法执行完毕后,才会执行下一个。
如果我们前后的方法由依赖关系,继发执行是没有问题的,但是如果并没有任何关系的话,这样就会很耗时,所以需要让这些await命令同时执行,也就是并发执行

1
2
3
4
5
6
7
8
// 方法 1 使用Promise.all
let results = await Promise.all([func1(), func2()])

// 方法 2
let func1Promise = func1()
let func2Promise = func2()
let res1 = await func1Promise
let res2 = await func2Promise

错误处理

如果await后面的异步操作出错,那么等同于async函数返回的 Promise 对象被reject。
防止出错的方法,也是将其放在try…catch代码块之中。

1
2
3
4
5
6
7
8
9
async function errtest() {
try {
await new Promise(function (resolve, reject) {
throw new Error('出错了');
});
} catch(e) {
}
return await('hello world');
}

如果有多个await命令,可以统一放在try…catch结构中。

1
2
3
4
5
6
7
8
9
10
11
12
async function errtest() {
try {
const val1 = await firstStep();
const val2 = await secondStep(val1);
const val3 = await thirdStep(val1, val2);

console.log('Final: ', val3);
}
catch (err) {
console.error(err);
}
}

注意点

  1. await 命令后面的 Promise 对象,运行结果可能是 rejected,所以最好把 await 命令放在 try…catch 代码块中。
  2. 使用async关键字声明异步函数。
  3. await 命令只能用在 async 函数之中,如果用在普通函数,就会报错。
  4. async用来申明里面包裹的内容可以进行同步的方式执行,await则是进行执行顺序控制,每次执行一个await,程序都会暂停等待await返回值,然后再执行之后的await。
  5. 多个await命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。

async/await 的优势

then 链

单一的 Promise 链并不能发现 async/await 的优势,但是,如果需要处理由多个 Promise 组成的 then 链的时候,优势就能体现出来了(很有意思,Promise 通过 then 链来解决多层回调的问题,现在又用 async/await 来进一步优化它)。

中间值

一个经常出现的场景是,我们先调起promise1,然后根据返回值,调用promise2,之后再根据这两个Promises得值,调取promise3。
对比一下Promise和async/await的实现方式,就可以发现 async/await代码非常简单,结构清晰

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Promise
const makeRequest = () => {
return promise1()
.then(value1 => {
// do something
return promise2(value1)
.then(value2 => {
// do something
return promise3(value1, value2)
})
})
}

// async/await
const makeRequest = async () => {
const value1 = await promise1()
const value2 = await promise2(value1)
return promise3(value1, value2)
}

还有很多的优点,就看大家实际项目中去挖掘了。
thank you

CSS Grid Layout (一)

Veröffentlicht am 2018-03-20 |

前言

在介绍Grid之前,先来说一下flex吧,引用阮大神的一句话:2009年,W3C 提出了一种新的方案—-Flex 布局,可以简便、完整、响应式地实现各种页面布局。目前,它已经得到了所有浏览器的支持,这意味着,现在就能很安全地使用这项功能。在这几年,flex布局已然成为前端的主流布局框架,每一个前端开发者都必须要掌握flexbox的用法。
然而,在2017年,Grid来势汹汹,声势浩大,相信大家就算没有在项目中使用过,也一定听过这个。Grid最大的特色,就是采用了二维布局,之前的布局方式都是采用一维布局。目前Grid的规范也逐渐完善,flex和grid各有千秋,日后这个后起之秀会不会成为web布局的扛把子,谁也说不准。

下面,就来介绍下本文的主角 Grid

简介已经支持情况

Grid简介

Grid:CSS网格布局(又名“网格”)是一个二维的基于网格的布局系统,其目的只在于完全改变我们设计基于网格的用户界面的方式。CSS一直用来布局网页,但一直都不完美。 一开始我们使用table 做布局,然后转向浮动、定位以及inline-block,但所有这些方法本质上都是 Hack 的方式,并且遗漏了很多重要的功能(例如垂直居中)。 Flexbox的出现在一定程度上解决了这个问题,但是它的目的是为了更简单的一维布局,而不是复杂的二维布局(Flexbox和Grid实际上一起工作得很好)。 只要我们一直在制作网站,我们就一直在为解决布局问题不断探索, 而Grid是第一个专门为解决布局问题而生的CSS模块。

浏览器支持情况

浏览器支持情况ß

Gird的基本概念

1.网格容器(Grid Container),对应Flexbox布局中的Flex容器

元素应用display:grid,它是其所有网格项的父元素。下面例子container就是网格容器。

1
2
3
4
5
6
7
8
9
<div class="container">
<div class="item item-1"></div>
<div class="item item-2"></div>
<div class="item item-3"></div>
</div>

.container {
display: grid;
}

2.网格项(Grid Item), 对应Flexbox布局中的Flex项目

网格容器的子元素,下面的item元素是网格项,但sub-item不是。

1
2
3
4
5
6
7
<div class="container">
<div class="item"></div>
<div class="item">
<p class="sub-item"></p>
</div>
<div class="item"></div>
</div>

3.网格线(Grid lines)

网格线是用来在水平和垂直方向分割网格的线。水平方向的网格线是从左向右;垂直方向是从上往下。网格线的编号都是从1开始的。

网格线

4.网格轨道(Grid Track)

两个相邻的网格线之间为网格轨道。你可以认为它们是网格的列或行。

5.网格单元(Grid Cell)

两个相邻的列网格线和两个相邻的行网格线组成的是网格单元,它是最小的网格单元。

6.网格区(Grid Area)

网格区是由任意数量网格单元组成。

父容器(Grid Container)的属性

display

将元素定义为 grid contaienr,并为其内容建立新的网格格式化上下文(grid formatting context)。
属性值:

  • grid: 生成块级网格
  • inline-grid: 生成行内网格
  • subgrid: 如果网格容器本身是网格项grid item(嵌套网格容器),此属性用来继承其父网格容器的列、行大小。
1
2
3
.container {
display: grid | inline-grid | subgrid;
}

注意:column, float, clear, 以及 vertical-align 对一个 grid container 没有影响

grid-template-columns / grid-template-rows (网格列和网格行)

使用以空格分隔的多个值来定义网格的列和行。这些值表示轨道大小,它们之间的空格代表表格线。当你设置行或列大小为auto时,网格会自动分配空间和网格线名称
eg:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 基本用法
.container {
grid-template-columns: 40px 50px auto 50px 40px;
grid-template-rows: 25% 100px auto;
}

// 可以给每条网格线命名
.container {
grid-template-columns: [first] 40px [line2] 50px [line3] auto [col4-start] 50px [five] 40px [end];
grid-template-rows: [row1-start] 25% [row1-end] 100px [third-line] auto [last-line];
}

 // 每条网格线也可以不止一个名字
.container {
grid-template-columns: [first] 40px [line2] 50px [line3] auto [col4-start] 50px [five] 40px [end];
grid-template-rows: [row1-start] 25% [row1-end row2-start] 100px [third-line] auto [last-line];
}

// 如果有重复的部分,可以使用repeat函数
.container {
grid-template-columns: repeat(3, 30px [col]) 40px;
}
等价于
.container {
30px [col] 30px [col] 30px [col] 40px;
}

可以使用fr单位可以将容器分为几等份,如果和实际值一起用,则代表分割剩余的部分,例如下面分成三等份

1
2
3
4
5
6
7
8
9
.container{
display:grid;
grid-template-columns: 1fr 1fr 1fr;
}

.container{
display:grid;
grid-template-columns: 1fr 100px 1fr 1fr;
}

网格项的属性

网格项属性

  • grid-column-start:设置网格项目垂直方向的开始位置网格线
  • grid-column-end:设置网格项目垂直方向的结束位置网格线
  • grid-row-start:设置网格项目水平方向的开始位置网格线
  • grid-row-end:设置网格项目水平方向的结束位置网格线
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<section class="grid-1">  
<div class="item-1">1</div>
<div class="item-2">2</div>
<div class="item-3">3</div>
<div class="item-4">4</div>
<div class="item-5">5</div>
<div class="item-6">6</div>
</section>

.grid-1 {
display: grid;
width: 100%;
max-width: 600px;
margin: 0 auto;
grid-template-columns: 300px 200px 100px;
grid-template-rows: 100px 50px;
}

.item-2 {
grid-column-start: 2;
grid-column-end: 4;
grid-row-start: 1;
grid-row-end: 2;
}

// 两个属性缩写
item-2 {
grid-column: 2 / 4;
grid-row: 1 / 2;
}

// 四个属性缩写
grid-row-start / grid-column-start/ grid-row-end / grid-column-end
item-2 {
grid-area: 1 / 2 / 2 / 4;
}

'结果'

合并单元格

其实上面的例子中已经实现了两个单元格合并(第二列和第三列),其实在Grid布局中提供了一个span关键词来实现单元格的跨越,span 后面跟的数字表示跨越多少个单元格。类似于table中的合并单元格,比如colspan合并列,rowspan合并行。

注意:如果没有设置grid-column-end或grid-row-end,默认将跨越一个轨道。项目也可以重叠,设置z-index来确定堆叠顺序。

1
2
3
4
5
6
7
8
9
10
// 等价与上面的合并第二列和第三列
.item-2 {
grid-column: 2 / span 2;
grid-row: 1;
}

或者
.item-2 {
grid-area: 2 / 1 / span 2;
}

单个网格项垂直于列网格线的对齐方式

属性值:

  • start: 网格区域左对齐。
  • end: 网格区域右对齐。
  • center: 网格区域居中。
  • stretch: 网格区域填满。
1
2
3
.item-1{
justify-self: start | end | center | stretch;
}

也可以给父容器设置justify-items,达到全部网格项对齐

单个网格项垂直于列网格线的对齐方式

属性值:

  • start: 网格区域顶部对齐。
  • end: 网格区域底部对齐。
  • center: 网格区域居中。
  • stretch: 网格区域填满。
1
2
3
.item-1{
align-self: start | end | center | stretch;
}

也可以在容器上设置align-items,达到全部网格项对齐

未完待续…

Webpack如何编写一个loader

Veröffentlicht am 2018-01-14 |

介绍

loader 用于对模块的源代码进行转换。loader 可以使你在 import 或”加载”模块时预处理文件。因此,loader 类似于其他构建工具中“任务(task)”,并提供了处理前端构建步骤的强大方法。loader 可以将文件从不同的语言(如 TypeScript)转换为 JavaScript,或将内联图像转换为 data URL。loader 甚至允许你直接在 JavaScript 模块中 import CSS文件!

这种链式调用和gulp跟相似,每一个loader都只关心自己的事,而不是一个loader完成所有的操作。

编写一个 loader

本地设置

  • 匹配(test)单个 loader,你可以简单通过在 rule 对象设置 path.resolve 指向这个本地文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    {
    test: /\.js$/
    use: [
    {
    loader: path.resolve('path/to/loader.js'),
    options: {/* ... */}
    }
    ]
    }
  • 匹配(test)多个 loaders,你可以使用 resolveLoader.modules 配置,webpack 将会从这些目录中搜索这些 loaders。例如,如果你的项目中有一个 /loaders 本地目录,在loaders下面有很多loader

    1
    2
    3
    4
    5
    6
    resolveLoader: {
    modules: [
    'node_modules',
    path.resolve(__dirname, 'loaders')
    ]
    }

loader 工具库

loader-utils :它提供了许多有用的工具,最常用的一种工具是获取传递给 loader 的选项。
schema-utils :配合 loader-utils,用于保证 loader 选项,进行与 JSON Schema 结构一致的校验。

开发

编写一个简单的json-loader

这个loader把简单的Excel表格导出为json对象。提供一个sheet属性,用于选择导出某一页的表格,默认是整个文档。
src/loader.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import { getOptions } from 'loader-utils';
import validateOptions from 'schema-utils';
import xlsx from '.json';

// Loader Mode
export const raw = true;

const schema = {
type: "object",
properties: {
sheet: {
anyOf: [
{ type: "number" },
{ type: "string" }
]
}
},
additionalProperties: true
}

export default function loader(source) {
const options = getOptions(this) || {};
validateOptions(schema, options, 'xlsx Loader');

source = JSON.stringify(xlsx.toJson(source, options.sheet || null))
.replace(/\u2028/g, '\\u2028')
.replace(/\u2029/g, '\\u2029')

return `export default ${ source }`;
}

raw: loader加载资源的模式,以二进制的方式加载文件,默认值是false,返回源文件内容的字符串。
schema:用于校验option选项。
xlsx.toJson:用于将源文件转换成json对象。

xlsx源码

github地址

测试用例

安装

我们将使用 Jest 框架。然后还需要安装 babel-jest 、 babel-preset-env 和 memory-fs:

1
npm install --save-dev jest babel-jest babel-preset-env

使用loader来处理test/example.xlsx文件

xlsx文件内容

id name age
1 biaoge 30
1 haohua 18

test/compiler.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import path from 'path';
import webpack from 'webpack';
import memoryfs from 'memory-fs';

export default (fixture, options = {}) => {
const compiler = webpack({
context: __dirname,
entry: `./${fixture}`,
output: {
path: path.resolve(__dirname),
filename: 'bundle.js',
},
module: {
rules: [{
test: /\.xls.?$/,
loader: path.resolve(__dirname, '../src/index'),
options
}]
}
});

compiler.outputFileSystem = new memoryfs();

return new Promise((resolve, reject) => {
compiler.run((err, stats) => {
if (err) reject(err);

resolve(stats);
});
});
}

最后,我们来编写测试,并且添加 npm script 运行它。

1
2
3
4
5
6
7
8
9
10
11
12
13
import compiler from './compiler';

test('getSingle', async () => {
const stats = await compiler('example.xlsx', {sheet: 'sheet1'});
const output = stats.toJson().modules[0].source;
expect(output).toBe(`export default [{"id":"1","name":"Jack","age":"20"},{"id":"2","name":"Tom","age":"18"}]`);
})

test('getAll', async () => {
const stats = await compiler('example.xlsx');
const output = stats.toJson().modules[0].source;
expect(output).toBe(`export default [{"sheet":"sheet1","data":[{"id":"1","name":"Jack","age":"20"},{"id":"2","name":"Tom","age":"18"}]}, {"sheet":"sheet2","data":[{"id":"1","name":"Jack","age":"20"},{"id":"2","name":"Tom","age":"18"}]}]`);
})

thank you

12

hupei

13 Artikel
© 2018 hupei
Erstellt mit Hexo
|
Theme — NexT.Mist v5.1.3