eslint-plugin-import 未找到webpack.config 错误

问题由来

项目是需要兼容多端(web、小程序),一些组件和辅助方法使用webpackmainFileds来区分各端兼容版本,因此一些组件文件夹内有package.json,如以下结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
project
├── src
│   ├── components
│   │   ├── swiper
│ │   │   ├── index.vue
│ │   │   ├── wx.vue
│ │   │   └── package.json
│   │   └── video
│ │      ├── index.vue
│ │      ├── wx.vue
│ │      └── package.json
│   └── pages
├── .eslintrc.js
├── webpack.config.js
└── package.json

另一方面,eslint-import-resolver-webpack在加载webpack.config时,如果配置中的地址不是绝对地址时候,会拼接上packageDir(通过find-root包,找到的当前package.json所在目录)

1
2
3
4
// https://github.com/benmosher/eslint-plugin-import/blob/master/resolvers/webpack/index.js#L342-L344
if (!path.isAbsolute(configPath)) {
configPath = path.join(packageDir, configPath)
}

而实际定义的webpack.config地址是相对/project/package.json的,这时候就会报出类似的错误。

1
Error resolving webpackConfig { Error: Cannot find module '/xxx/xxx/project/src/pages/components/swiper/webpack.config.js'

此处已经向作者提出issue, 提问“为什么要使用packageDir加入configPath,而不是使用eslint配置的目录”

解决方案

配置绝对地址webpack.config

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// .eslintrc.js
const path = require('path');

module.exports = {
// ...
settings: {
'import/resolver': {
webpack: {
// 配置为绝对地址
config: path.resolve(__dirname, 'shells/web/webpack.config.js'),
},
},
}
};

Promise 介绍

基础介绍

Promise 对象用于一个异步操作的最终完成(或失败)及其结果值的表示。(简单点说就是处理异步请求。我们经常会做些承诺,如果我赢了你就嫁给我,如果输了我就嫁给你之类的诺言。这就是promise的中文含义:诺言,一个成功,一个失败。)

MDN-Promise

Promise表示一个异步操作的结果,与之进行交互的方式主要是then方法,该方法注册了两个回调函数,用于接收promise的终值或本promise不能执行的原因。

如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var p = new Promise(function(resolve, reject){ // executor
if (Math.random() < 0.5) {
resolve('终值'); // fulfill
} else {
reject('据因'); // reject
}
});

p.then(
function(value){ // onFulfilled
console.log(value);
},
function(reason){ // onRejected
console.log(reason);
}
);

new Promise实例化一个Promise,其第一个构造参数executor是一个带有resolvereject两个参数的函数,executor函数在Promise构造函数返回新建对象前被调用,被传递resolvereject函数。resolvereject函数被调用时,分别将promise的状态改为fulfilled(完成)或rejected(失败)。executor内部通常会执行一些异步操作,一旦完成,可以调用resolve函数来将promise状态改成fulfilled,或者在发生错误时将它的状态改为rejected。

  • 接受(fulfill):指一个promise成功时进行的一系列操作,如状态的改变、回调的执行。虽然规范中用fulfill来表示接受,但在后世的promise实现多以resolve来指代之。
  • 拒绝(reject):指一个promise失败时进行的一系列操作。
  • 终值(eventual value):所谓终值,指的是promise被接受时传递给解决回调的值,由于promise有一次性的特征,因此当这个值被传递时,标志着promise等待态的结束,故称之终值,有时也直接简称为值(value)。
  • 据因(reason):也就是拒绝原因,指在promise被拒绝时传递给拒绝回调的值。

三个状态

三个状态:未被决议、完成、拒绝,决议完状态不再改变

  • pending: 等待态,移至执行态或拒绝态
  • fulfilled: 接受态,不能迁移至其他任何状态,必须拥有一个不可变的终值
  • rejected: 拒绝态,不能迁移至其他任何状态,必须拥有一个不可变的据因

这里的不可变指的是恒等(即可用 === 判断相等),而不是意味着更深层次的不可变(译者注:指当 value 或 reason 不是基本值时,只要求其引用地址相等,但属性值可被更改)。

promise

如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
var p = new Promise(function(resolve, reject) {
resolve(42);
reject('reason')
});
p.then(function(v) {
console.log(v); // 42
});
p.then(function(v) {
console.log(v); // 还是42
});
p.catch(function(reason) {
console.log(reason); // 不会执行
});

变量p为一个已经决议的promise,它的决议状态(PromiseStatus)一直都是resolved,值一直未42

Then

一个promise必须提供一个then方法以访问其当前值、终值和据因。

1
promise.then(onFulfilled, onRejected)

参数

  • onFulfilled:当Promise变成接受状态(fulfilled)时,该参数被调用。该函数有一个参数,即接受的值。
  • onRejected:当Promise变成拒绝状态(rejected)时,该参数被调用。该函数有一个参数,即拒绝的原因。

返回值

then方法返回一个Promise,而它的行为与then中的回调函数的返回值有关:

  • 如果then中的回调函数返回一个值,那么then返回的Promise将会成为接受状态,并且将返回的值作为接受状态的回调函数的参数值。
  • 如果then中的回调函数抛出一个错误,那么then返回的Promise将会成为拒绝状态,并且将抛出的错误作为拒绝状态的回调函数的参数值。
  • 如果then中的回调函数返回一个已经是接受状态的Promise,那么then返回的Promise也会成为接受状态,并且将那个Promise的接受状态的回调函数的参数值作为该被返回的Promise的接受状态回调函数的参数值。
  • 如果then中的回调函数返回一个已经是拒绝状态的Promise,那么then返回的Promise也会成为拒绝状态,并且将那个Promise的拒绝状态的回调函数的参数值作为该被返回的Promise的拒绝状态回调函数的参数值。
  • 如果then中的回调函数返回一个未定状态(pending)的Promise,那么then返回Promise的状态也是未定的,并且它的终态与那个Promise的终态相同;同时,它变为终态时调用的回调函数参数与那个Promise变为终态时的回调函数的参数是相同的。

如下代码:

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
var p1 = new Promise(function(resolve, reject) { // executor
if (Math.random() < 0.5) {
resolve('终值'); // fulfill
} else {
reject('据因'); // reject
}
});

var p2 = p1.then(
function(value) { // onFulfilled
console.log(value);
return new Promise(function(resolve) {
resolve(42);
});
},
function(reason){ // onRejected
console.log(reason);
return 21;
}
);

p2.then(function(value) {
console.log(value); // 42 或是 21
}, function(reason) {
console.log(reason); // 不会执行
})
  • 如果onFulfilled不是函数且p1成功执行,p2必须成功执行并返回相同的值
  • 如果onRejected不是函数且p1拒绝执行,p2必须拒绝执行并返回相同的据因

如下p1成功执行,p2onFulfilled不是函数,p2返回相同的值:

1
2
3
4
5
6
7
8
9
10
11
12
var p1 = Promise.resolve('终值'); // 等价于 new Promise(function(resovle) { resovle('终值') });
var p2 = p1.then(
undefined,
function(reason){ // onRejected
console.log(reason);
return 21;
}
);

p2.then(function(value) {
console.log(value); // '终值'
});

catch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var p1 = Promise.resolve('终值');

p1.then(
undefined,
function(reason){ // onRejected
console.log(reason);
return 21;
}
);

// 和上面等价
p1.catch(
function(reason){ // onRejected
console.log(reason);
return 21;
}
);

resolve

构造参数executorresolve接受一个参数(和then中的回调函数返回值处理流程一样):

  • 传递的不为promisethenable,则返回一个决议成功的promise,结果为传入的参数
  • 传递的promisethenable,则结果和此对象相同
1
2
var p = new Promise(function(resovle) { resovle('终值') });
var p2 = new Promise(function(resovle) { resovle(p) }); // Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: "终值"}

Promise.resolve

Promise.resolve提供一个简便的方法来得到接受的promise

1
2
3
Promise.resolve('终值');
// 和上面接近等价
new Promise(function(resovle) { resovle('终值') });

不同的是,如果Promise.resolve的入参promisethenable,则直接返回入参。

1
2
3
var a = new Promise(function(resovle) { resovle('终值') });
Promise.resolve(a) === a // true
new Promise(function(resovle) { resovle(a) }) === a // false

Promise.reject

类似Promise.reject是一个简便的方法来得到拒绝的promise

1
2
3
Promise.reject('终值');
// 和上面等价
new Promise(function(resovle, reject) { reject('据因') });

构造参数executorrejectPromise.reject接受一个参数,无论改参数是何种状态的promise还是普通值,都将其作为据因

其它细节

执行时间

【翻译】Promises/A+规范中对此解释的很清楚:

注1 这里的平台代码指的是引擎、环境以及promise的实施代码。实践中要确保onFulfilledonRejected方法异步执行,且应该在then方法被调用的那一轮事件循环之后的新执行栈中执行。这个事件队列可以采用“宏任务(macro-task)”机制或者“微任务(micro-task)”机制来实现。由于promise的实施代码本身就是平台代码(译者注: 即都是 JavaScript),故代码自身在处理在处理程序时可能已经包含一个任务调度队列。

译者注: 这里提及了macrotaskmicrotask两个概念,这表示异步任务的两种分类。在挂起任务时,JS引擎会将所有任务按照类别分到这两个队列中,首先在macrotask的队列(这个队列也被叫做task queue)中取出第一个任务,执行完毕后取出microtask队列中的所有任务顺序执行;之后再取macrotask任务,周而复始,直至两个队列的任务都取完。

两个类别的具体分类如下:

  • macro-task: script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering
  • micro-task: process.nextTick, Promises(这里指浏览器实现的原生 Promise), Object.observe, MutationObserver

详见stackoverflow 解答这篇博客

【翻译】Promises/A+规范 注1

补充一句:在处理微任务microtask时,他们可以排队更多的微任务,这些微任务都将被逐个运行,直到微任务队列耗尽。然后在再取macrotask任务。

考虑以下代码:

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
var p1 = new Promise(function(resovle) { // executor
console.log('resolved'); // 1. resolved
resovle([1, 2, 3]);
});

p1
.then(function(values) { // 第一个onFulfilled
console.log(values); // 3. [1, 2, 3]
return values.reduce(function(p, v) {
return p + v;
}, 0); // 求和
})
.then(function(sum) { // 第二个onFulfilled
console.log(sum); // 5. 6
throw new Error('据因');
})
.catch(function(reason) { // onRejected
console.log(reason); // 6. '据因'
});

p1.then(function () { // 第三个onFulfilled
console.log('第三个onFulfilled'); // 4. '第三个onFulfilled'
});

console.log('同步执行'); // 2. Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: 3}

setTimeout(function() {
console.log('setTimeout'); // 7. setTimeout
});

序号表示控制台打印的顺序,可以看出executor是同步执行的,在console.log('同步执行')执行后, 第一个onFulfilled、第三个onFulfilled、第二个onFulfilledonRejected陆续执行,最后才是setTimeout被执行。

再考虑以下代码,下面的示例展示了Promise.all的异步(当传递的可迭代对象是空时,是同步的):

1
2
3
4
5
6
7
8
9
var p1 = Promise.resolve(3);
var p2 = 1337;
var p3 = Promise.all([p1, p2]);

console.log(p3); // 1. Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}
console.log(Promise.all([])); // 2. Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: Array(0)}
setTimeout(function() {
console.log(p3); // 3. Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: 3}
});

可以看出p3在即使p1p2都是被接受的情况下还是未决议状态。 当script(整体代码)执行完成后,引擎再会去处理p3的决议(microtask)。 所有的microtask都处理完成后,再执行下一个macrotask(setTimeout)。

resolve和reject只接受一个参数

  • 如果使用多个参数,第一个参数之后的所有参数都会被忽略
  • 如果没有参数,valuereason则为undefined

没有决议,永远被挂着,不会执行

1
2
3
4
5
6
7
new Promise((resolve, reject) => {

}).then((res) => {
console.log(res); // 不会执行
}, (res) => {
console.log(res); // 不会执行
});

被吞掉的异常

以下代码会造成ReferenceError,然而没有处理。

1
2
3
4
5
6
7
new Promise(function(resolve, reject) {
resolve(a.b); // 运行后会提示 Uncaught (in promise) ReferenceError: a is not defined
// reject('123'); // 提示 Uncaught (in promise) 123
})
.then(function(v) {
console.log(v);
});

被拒绝的promise如果没有catch处理控制台会提示错误信息,但是不影响代码段继续执行, 这就是被吞掉的异常。被吞掉的异常较难察觉,无法记录。因此推荐所有的promise最后都加上catch;

1
2
3
4
5
6
7
8
9
10
new Promise(function(resolve, reject) {
resolve(a.b); // 运行后会提示 Uncaught (in promise) ReferenceError: a is not defined
// reject('123'); // 提示 Uncaught (in promise) 123
})
.then(function(v) {
console.log(v);
})
.catch(function(err) {
console.error(err);
});

Promise.all([])

Promise.all返回一个promise,只有在传入Promise.all所有promise完成, 返回的promise才会完成,并且其终值为所有promise终值组成的数组。 如果有任何promise拒绝,你只会得到第一个拒绝promise据因。 这种模式被称为: 只有所有人都到齐,门才会开。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var p1 = Promise.resolve(3);
var p2 = 1337; // 参数不是promise时, 表示解决
var p3 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, "foo");
});
Promise.all([p1, p2, p3]).then(values => {
console.log(values); // [3, 1337, "foo"]
});

// 加入被拒绝的promise
var p4 = Promise.all([p1, p2, p3, Promise.reject(555)]);

setTimeout(function() {
console.log(p4); // Promise {[[PromiseStatus]]: "rejected", [[PromiseValue]]: 555}
});

Promise.race([])

对于Promise.race来说,返回的promise只取决于只有第一个被决议promise, 并且其结果和被决议promise相同。这种模式被称为门闩:第一个到达者,打开门闩。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var p1 = Promise.resolve(3);
var p2 = 1337; // 参数不是promise时, 表示解决
var p3 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, "foo");
});
Promise.race([p1, p2, p3]).then(value => {
console.log(value); // 3
});

// 加入被拒绝的promise
var p4 = Promise.race([p1, p2, p3, Promise.reject(555)]);
var p5 = Promise.race([Promise.reject(555), p1, p2, p3]);

setTimeout(function() {
console.log(p4); // Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: 3}
console.log(p5); // Promise {[[PromiseStatus]]: "rejected", [[PromiseValue]]: 555}
});

异步链式流

then返回一个promise,可以链式调用:

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
function getLocation() {
return new Promise(function(resolve, reject) {
setTimeout(function() {
resolve({ latitude: 88, longitude: 30 });
// reject(new Error('获取失败'));
}, 1000)
});
}

function getNearShops(location) {
// 根据location查询附近的店
return new Promise(function() {
resolve([{ name: '五六面馆', id: 1, location }]);
// reject(new Error('获取失败'));
});
}

getLocation()
.then(getNearShops)
.then((nearShops) => {
console.log(nearShops);
})
.catch((err) => {
console.error(err); // log
});

更复杂的异步链式流

使用Promise我们能更清晰、更自信把功能拆分,不用担心回调被过多调用

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
function confirm(content) {
return new Promise(function(resolve, reject) {
model.confirm(content, ({ ok }) => {
if (ok) {
resolve();
} else {
reject(new Error('用户取消'));
}
})
})
}

var isAuthorizedLocation = false;
function authorizeLocation() {
if (isAuthorizedLocation) {
return Promise.resolve();
}
return confirm('是否授权定位') // 请求用户授权
.then(() => {
return new Promise(function(resolve, reject) { // 授权
// 去授权
resolve();
reject(new Error('授权失败'));
});
});
}

authorizeLocation() // 1. 授权
.then(getLocation) // 2. 获取定位
.then(getNearShops) // 3. 请求附近的店
.then((nearShops) => {
console.log(nearShops);
})
.catch((err) => {
console.error(err); // log
});

注意:如果Promise.race被传入空数组会被挂住,永远不会被决议,而Promise.all会立即完成。

参考

canvas-图片粒子效果

废话少说

实现以上效果需要4步

  1. 读取图片
  2. 对图片像素处理(偏移, 模糊…)
  3. 画到画布上
  4. 动画

1. 读取图片

rocket


1
2
3
4
// 新建一个image对象
const imgObj = new Image();
imgObj.onload = () => cb(imgObj);
imgObj.src = 'rocket.png';

2. 获取信息

canvas提供drawImage接口能把图片或是另一画布画在当前画布上,同时还有getImageData能从画布上获取某一块画布的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ctx.drawImage(imgObj, image.x, image.y, image.width, image.height); // 画到画布上

/**
* 从画布中获取颜色值
* { width: 100, height: 100, data: Uint8ClampedArray[40000] }
* .data [r1, g1, b1, a1, r2, g2, b2, a2]
*/
const imageData = ctx.getImageData(image.x, image.y, image.width, image.height);

/**
* calculateParticles
* 偏移
* [{ x, y, fillStyle, offsetX, offsetY, time }]
*/
const particles = calculateParticles(imageData.data, function generateStart() {
return {
x: canvas.width / 2,
y: canvas.height
}
});

3. 画到画布上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function draw() {
// 画每个点
for (let i = 0; i < particles.length; i++) {
const particle = particles[i];
// 时间
particle.time++;

// 设置填充颜色
ctx.fillStyle = particle.fillStyle;
// 绘粒子到画布上
ctx.fillRect(
particle.x,
particle.y,
2, 2);
}
}

4. 动画

缓动函数

指定动画效果在执行时的速度,使其看起来更加真实

ease.png

1
2
3
4
5
6
function easeInOutExpo(t, b, c, d) {
t /= d / 2;
if (t < 1) return c / 2 * Math.pow(2, 10 * (t - 1)) + b;

return c / 2 * ( -Math.pow(2, -10 * (t - 1)) + 2) + b;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function draw() {
// 画布刷新
ctx.fillStyle = backgroundColor;
ctx.fillRect(0, 0, width, height);

// 画每个点
for (let i = 0; i < particles.length; i++) {
const particle = particles[i];
// 时间
particle.time++;

// 设置填充颜色
ctx.fillStyle = particle.fillStyle;
// 绘粒子到画布上
ctx.fillRect(
easeInOutExpo(particle.time, particle.x, particle.offsetX, totalTime), // 使用 easeInOutExpo
easeInOutExpo(particle.time, particle.y, particle.offsetY, totalTime),
2, 2);
}

// 浏览器下一帧时,再绘画
requestAnimationFrame(this.draw);
}

总结

源码地址

优化

效果上

  • 粒子出发时间随机延迟
  • 粒子最终随机偏移
  • 加上拖影

性能上

  • 避免不必要的Canvas绘制状态频繁切换
  • 避免使用浮点数坐标

另一个随机效果

记一次微信开发者工具debug

在维护的项目Linux微信web开发者工具中,有多个issues报小程序请求返回不成功,并且终端报异常

1
2
3
4
5
6
7
8
9
10
11
12
[7031:7031:0601/201040:ERROR:CONSOLE(1588)] "TypeError: Cannot read property 'certificateDetailsPromise' of undefined TypeError: Cannot read property 'certificateDetailsPromise' of undefined
at r (<anonymous>:1:1116)
at WebInspector.NetworkManager.dispatchEventToListeners (chrome-devtools://devtools/bundled/inspector.js:737:185)
at WebInspector.NetworkDispatcher.responseReceived (chrome-devtools://devtools/bundled/inspector.js:7384:236)
at Object.dispatch (chrome-devtools://devtools/bundled/inspector.js:4608:63)
at WebInspector.MainConnection.dispatch (chrome-devtools://devtools/bundled/inspector.js:4548:31)
at WebInspector.MainConnection._dispatchMessage (chrome-devtools://devtools/bundled/inspector.js:10773:7)
at WebInspector.Object.dispatchEventToListeners (chrome-devtools://devtools/bundled/inspector.js:737:185)
at innerDispatch (chrome-devtools://devtools/bundled/inspector.js:1588:58)
at InspectorFrontendAPIImpl._dispatch (chrome-devtools://devtools/bundled/inspector.js:1587:1)
at DevToolsAPIImpl._dispatchOnInspectorFrontendAPI (devtools_compatibility.js:62:21)
at DevToolsAPIImpl.dispatchMessage (devtools_compatibility.js:147:14)", source: chrome-devtools://devtools/bundled/inspector.js (1588)

原因和解决方式如下:

本文运行环境为

  • nw.js: linux 0.19.4 chrome版本55
  • 小程序: 0.17.172600

package.nw/app/dist/inject/devtools.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
var a, r = function(e) {
/**
* @typedef {WebInspector.NetworkRequest}
*/
var t = e.data;
a || (a = document.getElementsByTagName("iframe")[0]);
// 1. 请求的安全信息
var n = t.securityDetails(),
o = { command: "securityDetails", url: t.url, statusCode: t.statusCode, remoteAddress: t._remoteAddress };
if (n) {
o.protocol = n.protocol, o.securityState = t.securityState();
new Date;
// 0. 在运行时t.target()并没有对象networkManager,在此引发错误
t.target().networkManager.certificateDetailsPromise(n.certificateId).then(function(e) {
var t = e.issuer ? e.issuer.toLocaleLowerCase() : "";
t.indexOf("rapidssl") === -1
&& t.indexOf("symantec") === -1
&& t.indexOf("geotrust") === -1
&& t.indexOf("thawte") === -1
&& t.indexOf("trustasia") === -1
|| (o.securityState = "secure");
a.contentWindow.postMessage(o, "*")
})
} else
a.contentWindow.postMessage(o, "*")
};

在注释0处发生上文终端所报异常,利用nw --remote-debugging-port=9222进行远程debug, 打印出t,类型为WebInspector.NetworkRequest,t.target()类型为WebInspector.Target 并没有属性networkManager,因此运行时类似

1
undefined.certificateDetailsPromise(n.certificateId).then(/* ... */)

根据WebInspectorNetworkRequest关键词能找到这里用的是chrome开发者工具的sdk, 项目地址为devtools-frontend

在此项目下,关键词certificateDetailsPromise出现的地方只有一处front_end/security/SecurityPanel.js:361

1
2
3
4
5
6
7
8
/**
* @typedef {Object}
* @property {!Protocol.Security.SecurityState} securityState - Current security state of the origin.
* @property {?Protocol.Network.SecurityDetails} securityDetails - Security details of the origin, if available.
* @property {?Promise<>} *certificateDetailsPromise* - Certificate details of the origin.
* @property {?Security.SecurityOriginView} originView - Current SecurityOriginView corresponding to origin.
*/
Security.SecurityPanel.OriginState;

Security.SecurityPanel.OriginState离我们的WebInspector.NetworkRequest有点距离,而解决这类问题我的宗旨是尽量少改源码,这条路有点远先放一旁。

上文异常代码中,在certificateDetailsPromise的then中获得到数据就是issuer

1
var t = e.issuer ? e.issuer.toLocaleLowerCase() : "";

那么issuer是什么含义呢?

devtools-frontend搜索下

这个关键字只出现了一次在 front_end/security/SecurityPanel.js:907

1
table.addRow(Common.UIString('Issuer'), originState.securityDetails.issuer);

而这里的originState.securityDetails和上文注释1处获取的t.securityDetails()好像是同一个,惊不惊喜,意不意外!!

1
var n = t.securityDetails(),

使用远程断点,看到n变量真有属性issuer,值为:”TrustAsia DV SSL CA - G5”

既然如此,现在的sdk是不是想让我们直接使用securityDetails,而不用通过certificateDetailsPromise来获取issuer呢?

将有问题的代码,修改为以下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 声明issuer
var issuer, t = e.data;

a || (a = document.getElementsByTagName("iframe")[0]);
var n = t.securityDetails(),
o = { command: "securityDetails", url: t.url, statusCode: t.statusCode, remoteAddress: t._remoteAddress };
if (n) {
console.error(n);

o.protocol = n.protocol, o.securityState = t.securityState();
new Date;
// 移除certificateDetailsPromise,从n取issuer
issuer = n.issuer ? n.issuer.toLocaleLowerCase() : "";
issuer.indexOf("rapidssl") === -1
&& issuer.indexOf("symantec") === -1
&& issuer.indexOf("geotrust") === -1
&& issuer.indexOf("thawte") === -1
&& issuer.indexOf("trustasia") === -1
|| (o.securityState = "secure"), a.contentWindow.postMessage(o, "*")
} else
a.contentWindow.postMessage(o, "*")

6/23更新: 为了替换更方便使用如下方式替换

t.target().networkManager.certificateDetailsPromise(n.certificateId) 替换为 Promise.resolve(n)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var a, r = function(e) {
var t = e.data;
a || (a = document.getElementsByTagName("iframe")[0]);
var n = t.securityDetails(),
o = { command: "securityDetails", url: t.url, statusCode: t.statusCode, remoteAddress: t._remoteAddress };
if (n) {
o.protocol = n.protocol, o.securityState = t.securityState();
new Date;
Promise.resolve(n).then(function(e) {
var t = e.issuer ? e.issuer.toLocaleLowerCase() : "";
t.indexOf("rapidssl") === -1
&& t.indexOf("symantec") === -1
&& t.indexOf("geotrust") === -1
&& t.indexOf("thawte") === -1
&& t.indexOf("trustasia") === -1
|| (o.securityState = "secure");
a.contentWindow.postMessage(o, "*")
})
} else
a.contentWindow.postMessage(o, "*")
};

运行成功

总结

查了下,issuer是指域名型证书型号,而TrustAsia(亚洲诚信)是一证书机构,”TrustAsia DV SSL CA - G5”是证书型号。

1
2
3
4
5
6
issuer.indexOf("rapidssl") === -1
&& issuer.indexOf("symantec") === -1
&& issuer.indexOf("geotrust") === -1
&& issuer.indexOf("thawte") === -1
&& issuer.indexOf("trustasia") === -1
|| (o.securityState = "secure")

rapidssl、symantec、geotrust、thawte和trustasia一样是各证书机构。 这部分代码的意思是只有当issuer中有这些机构关键字,就认为是有证书的,把securityState设置为”secure”,然后把信息传给业务层。

这次还有个需要纠结的地方是networkManager.certificateDetailsPromise()为毛没了,而小程序在用,mac和windows下没有问题,这是以后linux微信小程序升级需要注意的地方。

2difre-前端多工程协作环境配置

问题: 原来要打包到项目环境,得修改多个源文件(Gruntfile.js,http.js,config/dev.js),常常引发冲突. 实际上需要修改的是这三个配置:

  1. 跳转地址
  2. api基础地址
  3. 资源地址

另一个蛋疼的事是为了各项目协作,需要配置不需要修改的项目(还得经常合并master),部署到本地和项目环境.

以下用相对路径(所有路径都使用相对路径,特别是引入/api来表示api地址)来解决上述问题,操作步骤为以下两步

  1. 配置nginx分为本地开发环境和项目测试环境
  2. 项目修改
    • 修改Gruntfile.js,http.js,config/dev.js

常见url说明

常见静态资源请求url

1
2
http://api.l.whereask.com /invoice  /meal     /page/checkout.html
根地址 /变更分支 /工程名字 /资源地址

常见apiurl

1
2
http://api.l.whereask.com /invoice  /api         /orders/v1/get_query_shop_tax
根地址 /变更分支 /api特殊名字 /资源地址

nginx配置

本地开发配置

使用如下配置


*: 表示需要经常配置的部分

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
43
44
45
46
47
48
49
50
51
server {
listen 9981;
#rewrite_log on;
#access_log /var/log/nginx/2dfire.access.log;
#error_log /var/log/nginx/2dfire.error.log notice;

server_name 127.0.0.1;

location ^~ / {
# 所有api转到whereask
rewrite ^/([^/]*/api/.*)$ /whereask/$1 last;
# 其余重写到dev
rewrite ^/(.*)$ /dev/$1;
}
location /whereask/ { proxy_pass http://api.l.whereask.com/; }

# build后静态资源路径,选用
location /build/ {
rewrite ^/build/[^/]*/(shop|bill|meal|marketing|om)/(.*)$ /build/static-$1/release/min/$2 last;
rewrite ^/build/(.*)$ /whereask/$1 last;
# 静态地址
alias /c/src/js/;
}

# A. webpack热加载路径重写*
location /__webpack_hmr {
# 设置需要热加载的工程
rewrite ^/(.*)$ /dev/meal/$1;
}

# B. 开发地址部分,根据需要打开相应重写规则*
location /dev/ {
#1. `/daily/meal/` 经过 ``^~ /` 后为 `/dev/daily/meal/`
#2. 经过 `/dev/` 变为 `/dev/meal/`
#3. 同样shop是不匹配的,则 `/daily/shop/`重写为 `/whereask/daily/shop/`,使用whererask资源

#rewrite ^/dev/[^/]*/(shop/.*)$ /dev/$1 last;
#rewrite ^/dev/[^/]*/(bill/.*)$ /dev/$1 last;
#rewrite ^/dev/[^/]*/(marketing/.*)$ /dev/$1 last;
rewrite ^/dev/[^/]*/(meal/.*)$ /dev/$1 last;
#rewrite ^/dev/[^/]*/(om/.*)$ /dev/$1 last;
rewrite ^/dev/(.*)$ /whereask/$1 last;
}

# 本地webpack服务
location /dev/shop/ { proxy_pass http://localhost:8086/; }
location /dev/bill/ { proxy_pass http://localhost:8089/; }
location /dev/marketing/ { proxy_pass http://localhost:8085/; }
location /dev/meal/ { proxy_pass http://localhost:8088/; }
location /dev/om/ { proxy_pass http://localhost:8087/; }
}

可以实现

==>: 表示最后指向地址

1. 将含变更分支的url重写到本地webpack服务whereask变更环境

即实现根据变更分支自动使用对应变更静态资源

如根据以下配置

1
2
3
4
5
6
7
8
9
10
11
12
13
#开发地址部分,根据需要打开相应重写规则*
location /dev/ {
#1. `/daily/meal/` 经过 ``^~ /` 后为 `/dev/daily/meal/`
#2. 经过 `/dev/` 变为 `/dev/meal/`
#3. 同样shop是不匹配的,则 `/daily/shop/`重写为 `/whereask/daily/shop/`,使用whererask资源

#rewrite ^/dev/[^/]*/(shop/.*)$ /dev/$1 last;
#rewrite ^/dev/[^/]*/(bill/.*)$ /dev/$1 last;
#rewrite ^/dev/[^/]*/(marketing/.*)$ /dev/$1 last;
rewrite ^/dev/[^/]*/(meal/.*)$ /dev/$1 last;
#rewrite ^/dev/[^/]*/(om/.*)$ /dev/$1 last;
rewrite ^/dev/(.*)$ /whereask/$1 last;
}

可以将以下url重写到本地webpack服务

  • localhost:9981/invoice/meal/page/checkout.html ==> localhost:8088/page/checkout.html
  • localhost:9981/fire-one/meal/page/checkout.html ==> localhost:8088/page/checkout.html
  • localhost:9981/[任意变更分支]/meal/page/checkout.html ==> localhost:8088/page/checkout.html

将以下非meal工程url反向代理到http://api.l.whereask.com/invoice/...部分

  • localhost:9981/invoice/om/page/om.html ==> api.l.whereask.com/invoice/om/page/om.html

2. 将webpack热加载路径重写

  • localhost:9981/webpack_hmr ==> localhost:8088/webpack_hmr

3. 将apiurl反向代理到http://api.l.whereask.com/变更分支/api/...部分

即实现根据变更分支自动使用对应变更api资源

  • localhost:9981/invoice/api/orders/v1/get_query_shop_tax ==> api.l.whereask.com/invoice/api/orders/v1/get_query_shop_tax

项目环境配置(10.1.7.159)

项目环境各分支作用说明

分支名称 作用
daily 日常环境,大家都可以上的车,代码从master中检出
dev_193 可以支付的分支

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 工程默认使用daily代码
location /invoice/ {
rewrite ^/[\w]*?/(.*)$ /daily/$1 last;
}

# 真实api地址 重写到server_from_meal
location /invoice/api/ {
rewrite ^/invoice/api/(.*)$ /invoice_server/$1;
}

# 假设meal和bill是本次变更会修改的工程,添加以下两条
location /invoice/meal/ {
proxy_pass http://10.1.4.186/nginx/meal/;
}

location /invoice/bill/ {
proxy_pass http://10.1.4.187/nginx/bill/;
}

配置dev_193

如果需要dev_193使用invoice分支代码,可以如下修改dev_193配置

1
2
3
4
location /dev_193/ {
#全部重写到invoice
rewrite ^/dev_193/(.*)$ /invoice/$1;
}

项目修改细节

static-meal, static-shop, static-om, static-bill, static-markting在项目中都需要做以下修改(只涉及到dev环境打包)

Gruntfile.js

删除dev环境替换相对路径的配置


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
43
44
45
46
47
48
49
50
51
52
53
54
55
{
// ...
dev: {
replacements: [
/* 为了页面间传递url,需要修改../page为../../{当前工程}/page */
{
from: /(\.\.\/page)/g,
to: '../../meal/page'
},
/* 从这开始删除
{
from: /(\.\.\/public)/g,
to: 'http://10.1.4.186/nginx/meal/public'
},
{
from: /(\.\.\/images)/g,
to: 'http://10.1.4.186/nginx/meal/images'
},
{
from: /(\.\.\/css)/g,
to: 'http://10.1.4.186/nginx/meal/css'
},
{
from: /(\.\.\/js)/g,
to: 'http://10.1.4.186/nginx/meal/js'
},
{
from: /(\.\.\/page)/g,
to: 'http://api.l.whereask.com/fromMeal/meal/page'
},
{
from: /(\.\.\/\.\.\/marketing)/g,
to: 'http://api.l.whereask.com/fromMeal/marketing'
},
{
from: /(\.\.\/\.\.\/bill)/g,
to: 'http://api.l.whereask.com/fromMeal/bill'
},
{
from: /(\.\.\/\.\.\/om)/g,
to: 'http://api.l.whereask.com/fromMeal/om'
},
{
from: /(\.\.\/\.\.\/shop)/g,
to: 'http://api.l.whereask.com/fromMeal/shop'
},
{
from: 'grunt_env_dev',
to: 'grunt_env_dev'
}
结束 */
]
}
// ...
}

http.js

修改api基础地址为相对路径

1
2
3
4
5
// var API_BASE_URL = 'http://api.l.whereask.com/server_from_meal';
var API_BASE_URL = '../../api';

// var RETAIL_BASE_URL = 'http://retailweixin.2dfire-dev.com/retail-weidian-api';
var RETAIL_BASE_URL = '../../retail-weidian-api';

config/dev.js

修改api基础地址为相对路径

1
2
3
4
5
6
7
8
9
10
11
module.exports = {
// API_BASE_URL: 'http://api.l.whereask.com/server_from_meal',
API_BASE_URL: '../../api',

// RETAIL_BASE_URL: 'http://retailweixin.2dfire-dev.com/retail-weidian-api',
RETAIL_BASE_URL: '../../retail-weidian-api',

SHARE_BASE_URL: 'http://api.l.whereask.com',
IMAGE_BASE_URL: 'http://ifiletest.2dfire.com/',
API_WEB_SOCKET: 'http://10.1.5.114:9003/web_socket'
};

css3 progress bar

实现的效果

原理

  1. 用css3的linear-gradient在节点的背景上画一个渐变效果;
  2. 给背景一个动画
  3. 结束

源码

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
43
44
45
46
47
48
49
50
51

<div class="progress-bar"></div>

<style type="text/css">
@keyframes progress_bar_key_frames {
from {
transform: translateX(-100%);
}
to {
transform: translateX(100%);
}
}
@keyframes progress_bar_scale {
from {
transform: scaleX(0);
}
to {
transform: scaleX(1);
}
}
.progress-bar {
width: 100%;
height: 20px;
background-color: white;
overflow: hidden;
position: relative;
}

.progress-bar:before, .progress-bar:after {
height: 100%;
display: block;
content: '';
position: absolute;
top: 0px;
left: 0px;
width: 100%;
}
.progress-bar:before {
background-color: #3ba776;
transform-origin: left center;
transform: scaleX(1);
/*transition: transform 1s ease-out;*/
animation: progress_bar_scale 20s infinite;
}
.progress-bar:after {
background-repeat: no-repeat;
animation: progress_bar_key_frames 2s ease-out infinite;
/*// animation-direction: alternate;*/
background-image: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.6) 90%, transparent 100% );
}
</style>

eslint

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
{
"root": true,
"env": {
"es6": true,
"node": true,
"browser": true,
"commonjs": true,
"jquery": true
},
"plugins": [
"import",
"html",
"react"
],
"extends": [
"eslint:recommended",
"plugin:import/errors",
"plugin:import/warnings",
"plugin:react/recommended"
],
"settings" : {
"import/resolver": {
"webpack": {
"config": "build/webpack.dev.js"
}
}
},
"parserOptions": {
"ecmaFeatures": {
"experimentalObjectRestSpread": true,
"jsx": true
},
"sourceType": "module"
},
"globals": {
},
"rules": {
"react/react-in-jsx-scope": ["off"],
"react/prop-types": ["off"],
"indent": [
"error",
4
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"off",
"single"
],
"semi": [
"error",
"always"
]
}
}

文件是否存在判断

插件eslint-plugin-import

增加配置

1
2
3
4
5
6
7
8
9
10
{
"plugins": [
"import"
],
"extends": [
"eslint:recommended",
"plugin:import/errors",
"plugin:import/warnings",
]
}

兼顾Webpack

插件eslint-import-resolver-webpack

增加配置

1
2
3
4
5
6
7
8
9
{
"settings" : {
"import/resolver": {
"webpack": {
"config": "build/webpack.dev.js"
}
}
},
}

react支持

插件eslint-plugin-react

增加配置

1
2
3
4
5
6
7
8
{
"plugins": [
"react"
],
"extends": [
"plugin:react/recommended",
]
}

集成

sublime

插件推荐

  • SublimeLinter + SublimeLinter-eslint
  • Eslint-Formater

安装SublimeLinter后按需要修改环境地址

1
2
3
4
5
6
7
8
9
10
11
{
"user": {
"paths": {
"linux": [
"/usr/local/bin"
],
"osx": [],
"windows": []
}
}
}

sublime快捷键备忘

Ctrl+Shift+P:打开命令面板

Ctrl+P:搜索项目中的文件

Ctrl+G:跳转到第几行

Ctrl+W:关闭当前打开文件

Ctrl+Shift+W:关闭所有打开文件

Ctrl+Shift+V:粘贴并格式化

Ctrl+D:选择单词,重复可增加选择下一个相同的单词

Ctrl+L:选择行,重复可依次增加选择下一行

Ctrl+Shift+L:选择多行

Ctrl+Shift+Enter:在当前行前插入新行

Ctrl+X:删除当前行

Ctrl+M:跳转到对应括号

Ctrl+U:软撤销,撤销光标位置

Ctrl+J:选择标签内容

Ctrl+F:查找内容

Ctrl+Shift+F:查找并替换

Ctrl+H:替换

Ctrl+R:前往method

Ctrl+N:新建窗口

Ctrl+K+B:开关侧栏

Ctrl+Shift+M:选中当前括号内容,重复可选着括号本身

Ctrl+F2:设置/删除标记

Ctrl+/:注释当前行

Ctrl+Shift+/:当前位置插入注释

Ctrl+Alt+/:块注释,并Focus到首行,写注释说明用的

Ctrl+Shift+A:选择当前标签前后,修改标签用的

F11:全屏

Shift+F11:全屏免打扰模式,只编辑当前文件

Alt+F3:选择所有相同的词

Alt+.:闭合标签

Alt+Shift+数字:分屏显示

Alt+数字:切换打开第N个文件

Shift+右键拖动:光标多不,用来更改或插入列内容

??鼠标的前进后退键可切换Tab文件

Ctrl+依次点击或选取:可需要编辑的多个位置

Ctrl+Shift+上下键:可替换行

选择类

Ctrl+D:选中光标所占的文本,继续操作则会选中下一个相同的文本。

Alt+F3:选中文本按下快捷键,即可一次性选择全部的相同文本进行同时编辑。举个栗子:快速选中并更改所有相同的变量名、函数名等。

Ctrl+L:选中整行,继续操作则继续选择下一行,效果和Shift+↓效果一样。

Ctrl+Shift+L:先选中多行,再按下快捷键,会在每行行尾插入光标,即可同时编辑这些行。

Ctrl+Shift+M:选择括号内的内容(继续选择父括号)。举个栗子:快速选中删除函数中的代码,重写函数体代码或重写括号内里的内容。

Ctrl+M:光标移动至括号内结束或开始的位置。

Ctrl+Enter:在下一行插入新行。举个栗子:即使光标不在行尾,也能快速向下插入一行。

Ctrl+Shift+Enter:在上一行插入新行。举个栗子:即使光标不在行首,也能快速向上插入一行。

Ctrl+Shift+[:选中代码,按下快捷键,折叠代码。

Ctrl+Shift+]:选中代码,按下快捷键,展开代码。

Ctrl+K+0:展开所有折叠代码。

Ctrl+←:向左单位性地移动光标,快速移动光标。

Ctrl+→:向右单位性地移动光标,快速移动光标。

shift+↑:向上选中多行。

shift+↓:向下选中多行。

Shift+←:向左选中文本。

Shift+→:向右选中文本。

Ctrl+Shift+←:向左单位性地选中文本。

Ctrl+Shift+→:向右单位性地选中文本。

Ctrl+Shift+↑:将光标所在行和上一行代码互换(将光标所在行插入到上一行之前)。

Ctrl+Shift+↓:将光标所在行和下一行代码互换(将光标所在行插入到下一行之后)。

Ctrl+Alt+↑:向上添加多行光标,可同时编辑多行。

Ctrl+Alt+↓:向下添加多行光标,可同时编辑多行。

编辑类

Ctrl+J:合并选中的多行代码为一行。举个栗子:将多行格式的CSS属性合并为一行。

Ctrl+Shift+D:复制光标所在整行,插入到下一行。

Tab:向右缩进。

Shift+Tab:向左缩进。

Ctrl+K+K:从光标处开始删除代码至行尾。

Ctrl+Shift+K:删除整行。

Ctrl+/:注释单行。

Ctrl+Shift+/:注释多行。

Ctrl+K+U:转换大写。

Ctrl+K+L:转换小写。

Ctrl+Z:撤销。

Ctrl+Y:恢复撤销。

Ctrl+U:软撤销,感觉和Gtrl+Z一样。

Ctrl+F2:设置书签

Ctrl+T:左右字母互换。

F6:单词检测拼写

搜索类

Ctrl+F:打开底部搜索框,查找关键字。

Ctrl+shift+F:在文件夹内查找,与普通编辑器不同的地方是sublime允许添加多个文件夹进行查找,略高端,未研究。

Ctrl+P:打开搜索框。举个栗子:1、输入当前项目中的文件名,快速搜索文件,2、输入@和关键字,查找文件中函数名,3、输入:和数字,跳转到文件中该行代码,4、输入#和关键字,查找变量名。

Ctrl+G:打开搜索框,自动带:,输入数字跳转到该行代码。举个栗子:在页面代码比较长的文件中快速定位。

Ctrl+R:打开搜索框,自动带@,输入关键字,查找文件中的函数名。举个栗子:在函数较多的页面快速查找某个函数。

Ctrl+::打开搜索框,自动带#,输入关键字,查找文件中的变量名、属性名等。

Ctrl+Shift+P:打开命令框。场景栗子:打开命名框,输入关键字,调用sublime text或插件的功能,例如使用package安装插件。

Esc:退出光标多行选择,退出搜索框,命令框等。

查找模式下

Alt+R:正则模式 Alt+C:严格大小写 Alt+W:一个单词 F3:查找下一个 Shift+F3:查找上一个 Alt+Enter:查找所有 Ctrl+Alt+Enter:替换所有 Ctrl+Shift+H:替换

显示类

Ctrl+Tab:按文件浏览过的顺序,切换当前窗口的标签页。

Ctrl+PageDown:向左切换当前窗口的标签页。

Ctrl+PageUp:向右切换当前窗口的标签页。

Alt+Shift+1:窗口分屏,恢复默认1屏(非小键盘的数字)

Alt+Shift+2:左右分屏-2列

Alt+Shift+3:左右分屏-3列

Alt+Shift+4:左右分屏-4列

Alt+Shift+5:等分4屏

Alt+Shift+8:垂直分屏-2屏

Alt+Shift+9:垂直分屏-3屏

Ctrl+K+B:开启/关闭侧边栏。

F11 全屏模式

Shift+F11:免打扰模式

update

Ctrl+k+2:折叠注释和方法

Ctrl+k+3:折叠if

Ctrl+k+4:折叠switch