SegmentFault 青青子衿最新的文章
2019-10-05T18:01:36+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
位操作及其应用
https://segmentfault.com/a/1190000020597600
2019-10-05T18:01:36+08:00
2019-10-05T18:01:36+08:00
Aria
https://segmentfault.com/u/janesu
5
<p>我在看lodash实现一些工具函数的源码时发现lodash定义了一些<code>bitMask</code>的常量。我一开始没弄明这是什么鬼东西,用Google搜了一圈才发现是我之前接触过得位操作运算一类的东西。并且源码和我搜索的资料给我提供了另一种使用场景,感觉应用性还是蛮强的,所以干脆总结一下好了。</p>
<p>先以下面的表达式展开需要了解的基础知识。</p>
<pre><code class="js">// lodash 源码里定义的常量
var CLONE_DEEP_FLAG = 1</code></pre>
<p>JavaScript遵循 IEEE 754 标准,无论是整数还是小数都是用双精度浮点数表述,双精度浮点数8个字节,表示64位二进制位,所以双精度浮点数的表示范围是<code>-2^63 ~ 2^63-1</code>。但是在进行位操作时则是用的32位数表示,也就是4个字节,表示范围为`-2^31 ~ 2^31-1,其中无论是32位还是64位,最高位都是符号位,0表示正数,1表示负数。</p>
<p>上面的表达式在进行位操作就会转换成下面这种,如果超过32位了,那超过的部分就会全部省去。</p>
<pre><code class="js">00000000 00000000 00000000 00000001</code></pre>
<p>下面介绍几种常用的操作符。</p>
<h2>&(位与)</h2>
<pre><code class="js">let a = 1,
b = 2
console.log(a & b) // 0
// 0001
// 0010
// = 0000</code></pre>
<p>把变量<code>a</code>和变量<code>b</code>都展开成32位二进制数,省去前面的0,<code>a</code>的二进制表示为<code>0001</code>,<code>b</code>为<code>0010</code>,接着就是对应位数的二进制位比较,如果相同就是1,否则为0。</p>
<h2>|(位或)</h2>
<pre><code class="js">let a = 1,
b = 2
console.log(a | b) // 3
// 0001
// 0010
// = 0011</code></pre>
<p>参照上面一种,不同的是相同的二进制位上,只要有一个是1,则结果就是1,所以就是<code>0011</code>。</p>
<h2>^(位异或)</h2>
<p>这个和<code>|</code>有点区别,相同的地方在于如果同一位数上的数只要一个是1,则这个位数的结果就是1,不同的地方在于相同的位数上如果数值相同,则结果为0.</p>
<pre><code class="js">let a = 1,
b = 2
console.log(a ^ b) // 3
// 0001
// 0010
// = 0011
let c = 3,
d = 3
console.log(a ^ b) // 0
// 0011
// 0011
// = 0000</code></pre>
<h2>~(位异或)</h2>
<p>这个和之前三个最大的区别是对单个数的操作,而不是两个数的比较结果。简单来说就是取反,对二进制上的每一位都取反。但是这里有个有趣的现象。</p>
<pre><code class="js">let a = 1;
console.log(~ a) // -2</code></pre>
<p>无论用<code>~</code>取反任何数,得出的都是负数,而且是在正数上加一的负数。这里涉及三个概念:<code>原码</code>,<code>反码</code>,<code>补码</code>。首先明确一点,<strong>负数是以补码的形式存在的</strong>。</p>
<h3>原码</h3>
<p>正数和负数的都是转换成二进制数后的样子,不同的是负数的原码在最高位+1。</p>
<pre><code class="js">// 4的原码
00000000 000000000 000000000 00000100
// -4的原码
10000000 000000000 000000000 00000100</code></pre>
<h3>反码</h3>
<p>正数的反码和正数的原码一致。但负数的反码是对除了符号位上的其他二进制位取反。</p>
<pre><code class="js">// 4的反码
00000000 000000000 000000000 00000100
// -4的反码
11111111 11111111 11111111 11111011</code></pre>
<h3>补码</h3>
<p>正数的补码还是和正数的原码一致,但负数的补码是在负数反码的基础上对最后一位加1。</p>
<pre><code class="js">// 4的补码
00000000 000000000 000000000 00000100
// -4的补码
11111111 11111111 11111111 11111100</code></pre>
<p>所以说回上面提到的问题,3被<code>~</code>转换为<code>-4</code>的过程。</p>
<pre><code class="js">3:00000000 00000000 00000000 00000100
~3:11111111 11111111 11111111 11111011
// ~3 这时候表示的是负数,那就按照原码->补码的顺序倒推
// 1.最低的位数-1
11111111 11111111 11111111 11111010
// 2.取反(除了符号位)
10000000 00000000 00000000 00000101
// 3.转换成十进制
-4</code></pre>
<h2>应用</h2>
<p>介绍完了几种操作符,来说说有啥应用。</p>
<h3>136. 只出现一次的数字</h3>
<p>这是LeetCode上的一道题,题目是这样写的:</p>
<blockquote>给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。</blockquote>
<p>这个用上面介绍过的<code>^(异或)</code>操作符是最容易解答的,可以可以下<code>^</code>的特性。题目里说的是只有一个数是唯一的,其他都是两两出现,而相同的数字,就像我上面举例用了两个3,结果是0,因为每个二进制上的数都相同,所以结果就是每个位上都变成了0.</p>
<pre><code class="js">function onlyNums(arr){
return arr.reduce((all,item) => all ^ item)
}</code></pre>
<h3>权限</h3>
<p>这个例子就和我们的日常贴的比较近了。后台系统进行权限配置的时候,一般可能就是定义几个字符串定义不同的权限,如果一个人同时有很多权限,结果可能是个数组,也可能是把不同权限字符串拼接成新的字符串。</p>
<pre><code class="js">let permission1 = 001 // 登录权限
let permission2 = 002 // 创建权限
let permission3 = 003 // 删除权限
let adminPermission = '001,002,003'
// or
let adminPermission = [001,002,003]</code></pre>
<p>如果换成位操作的思路:</p>
<pre><code class="js">let permission1 = 1 // 登录权限
let permission2 = 2 // 创建权限
let permission3 = 4 // 删除权限
let permission4 = 8 // 编辑权限
let adminPermission = (permission1 | permission2 | permission3)
</code></pre>
<p>这个时候检查<code>adminPermission</code>是否拥有某个权限就可以这样:</p>
<pre><code class="js">if((adminPermission & permission1) === permission1){
// 有登录权限
}</code></pre>
<p>删除某个权限:</p>
<pre><code class="js">adminPermission = adminPermission & (~ permission2)</code></pre>
<p>新增某个权限:</p>
<pre><code class="js">adminPermission = adminPermission | permission4</code></pre>
<p>除了上面两种场景,位操作还用于加密算法中,但是我的工作没有涉及过这方面,所以就不具体展开说了。感兴趣的话可以自己看看。</p>
Promise知识的一些边角料
https://segmentfault.com/a/1190000020439215
2019-09-20T02:13:23+08:00
2019-09-20T02:13:23+08:00
Aria
https://segmentfault.com/u/janesu
0
<p>Promise现在已经成为日常开发绕不过去的一个API了,并且也是面试中最喜欢被问到的部分,所以相信大家对它都有一个最基本的认识。所以我并不会再详细介绍这个API的方方面面,而是说一些可能大家日常没有注意到的地方。</p>
<p>Promise作为处理异步函数更好的解决方案,优点在于他有一个初始状态,并且状态发生变化之后就不会再改变,也就是<code>pendding(等待)</code>,<code>resolve(完成)</code>和<code>reject(拒绝)</code>。</p>
<p>当执行<code>new Promise(...)</code>之后,返回的是一个<code>Promise</code>对象,虽然这个代码写了不知道多少遍,但是我并没有想过一个问题:<code>Promise</code>对象它有什么特点?怎么才能算一个<code>Promise</code>对象?</p>
<p>查了一些资料之后了解到,<code>Promise</code>对象是具有thenable特征的对象,也就是这个对象上具有<code>then</code>这个属性,不论这个属性是属于对象自身还是存在于原型链的某一处。所以总结一下就是,<code>thenable</code>对象不一定是<code>Promise</code>对象,但是<code>Promise</code>对象一定具有<code>thenable</code>特征。</p>
<p>可以看下面这段代码:</p>
<pre><code class="js">let p = new Promise((resolve,reject)=>{})
if(p !== null && (typeof p === 'object' || typeof p === 'function') && typeof p.then === 'function'){
// thenable 对象
}else{
// 非 thenable 对象
}</code></pre>
<p>接下来就是我想说的关键部分,也就是<code>resolve</code>和<code>reject</code>。平常写的代码可能都是下面这几种:</p>
<pre><code class="js">let something
let p = new Promise((resolve,reject)=>{
// 第一种
resolve(something)
// 第二种
reject(something)
}).then(resolveCallback,errorCallback)
// 第三种
Promise.resolve(something).then(resolveCallback)
// 第四种
Promise.reject(something).then(errorCallback)</code></pre>
<p><code>Promise</code>本身代表着一种承诺,而且是指向未来的,所以他就有可能成功有可能失败。<code>reject</code>明确表示的是失败状态,然而<code>resolve</code>的翻译是<strong>处理完成</strong>,这里隐含的意思是并不是明确表示这个承诺就一定成功。</p>
<p>所以对于上面的代码,当<code>something</code>是一个常量(比如数字),根据执行的方法,<code>resolveCallback</code>或者<code>errorCallback</code>就会被执行。了解过少许<code>Promise</code>的原理就会知道,执行<code>Promise.resolve()</code>,js会将传入的参数转换为<code>Promise</code>对象返回,那如果传去的不是一个常量而是一个新的<code>Promise</code>对象又会如何?</p>
<pre><code class="js">let p1 = Promise.resolve('1')
let p2 = Promise.resolve(p1)
p2.then(res=>{
console.log(res) // 这里返回的是什么呢
})</code></pre>
<p>大家可以试着运行一下,结果是1。如果想不明白原因,可以看下面这部分:</p>
<pre><code class="js">let p1 = Promise.resolve('1')
let p2 = Promise.resolve(p1)
console.log(p1 === p2)</code></pre>
<p>此时的结果是<code>true</code>,也就是说把一个<code>Promise</code>对象作为参数传给<code>Promise.resolve</code>,返回的是依旧是传入的<code>Promise</code>对象。以下的结果也是一样:</p>
<pre><code class="js">let p1 = new Promise((resolve, reject) => {
resolve(1)
})
let p2 = Promise.resolve(p1)
console.log(p1 === p2) // true</code></pre>
<p>但是如果把上面代码中的<code>resolve</code>改成<code>reject</code>,则判断条件就是<code>false</code>。也就是说<code>reject</code>并不具备这个特性。</p>
<p>前面说过,<code>Promise</code>对象是具有<code>thenable</code>特性的对象,那现在传入<code>Promise.resolve</code>里的如果就是一个具备<code>then</code>属性的对象,又会如何呢?</p>
<pre><code class="js">let o = {
then(resolve,reject){
}
}
Promise.resolve(o)</code></pre>
<p>这里什么都不会发生,此时如果打印<code>Promise.resolve(o)</code>,会发现控制台显示这是一个处于<code>pendding</code>状态的<code>Promise</code>。</p>
<p>用过<code>Promise</code>的知道,它具备一个<code>then</code>方法,有两个参数,第一个是表示<code>resolve</code>的回调函数,第二个是表示<code>reject</code>的回调函数。所以尝试改一下上面的代码。</p>
<pre><code class="js">let o = {
then(resolve,reject){
resolve(1)
}
}
Promise.resolve(o).then(res=>{
console.log(res) // 1
})</code></pre>
<p>当执行对象o上的then属性里的第一个回调函数,作用就类似于调用构造函数是调用的<code>resolve</code>回调函数。</p>
<p>接下来可以自己尝试以下几种情况:</p>
<ol>
<li>将对象o上的<code>resolve()</code>改成<code>reject()</code>,观察执行then方法的结果</li>
<li>在o的then里同时调用<code>reject()</code>和<code>resolve()</code>,并且调回先后位置。观察执行then后的结果。</li>
</ol>
<p>总结一下,不论是<code>Promise.resolve()</code>还是构造函数中执行<code>resolve()</code>,传入的如果既不是<code>Promise</code>对象,也不是<code>thenable</code>对象,则返回的是一个完成状态的<code>Promise</code>对象。如果传入的是一个<code>Promise</code>对象,返回的是就是该对象。如果传入的是一个<code>thenable</code>对象,则返回的是作为<code>Promise</code>展开的<code>thenable</code>对象。</p>
<p>那说回开头提到的,<code>resolve</code>标记的之所以是一种完成状态,而不是成功,就在于如果传入的参数为<code>Promise</code>对象或者<code>thenable</code>对象时,如果这两者内部标记的状态为<code>reject</code>,即便外面调用的是<code>resolve</code>,但执行的却是<code>errorCallback</code>。因此平时说<code>Promise</code>的其中一种状态是成功的说法就是错误的。</p>
vue源码分析之nextTick
https://segmentfault.com/a/1190000020412382
2019-09-17T20:53:40+08:00
2019-09-17T20:53:40+08:00
Aria
https://segmentfault.com/u/janesu
11
<p>Vue中有个API是<code>nextTick</code>,官方文档是这样介绍作用的:</p>
<blockquote>将回调延迟到下次 DOM 更新循环之后执行。在修改数据之后立即使用它,然后等待 DOM 更新。</blockquote>
<p>理解这部分内容,有助于理解Vue对页面的渲染过程,同时也可以了解到<code>beforeUpdate</code>和<code>updated</code>的使用。另外就是通过了解<code>nextTick</code>的调用了解vue内部是如何使用<code>Promise</code>的。这部分内容和之前介绍计算属性的内容也有关联,可以比照着看。</p>
<p>首先看一下我创建的例子:</p>
<pre><code class="html"> <!-- HTML 部分 -->
<div id="test">
<p>{{ name }}的年龄是{{ age }}</p>
<!-- <p>
{{ info }}
</p> -->
<div>体重<input type="text" v-model="age" /></div>
<button @click="setAge">设置年龄为100</button>
</div></code></pre>
<pre><code class="js"> // js 部分
new Vue({
el: '#test',
data() {
return {
name: 'tuanzi',
age: 2
}
},
beforeUpdate() {
console.log('before update')
debugger
},
updated() {
console.log('updated')
debugger
},
methods: {
setAge() {
this.age = 190
debugger
this.$nextTick(() => {
console.log('next tick', this.age)
debugger
})
}
}
})</code></pre>
<p>当页面渲染完成,点击按钮触发事件之后,都会发生什么呢~~</p>
<p>直接介绍计算属性的时候说过,当页面初次加载渲染,会调用模板中的值,这时会触发该值的<code>getter</code>设置。所以对于我们这里,data中的<code>name</code>和<code>age</code>都会订阅<code>updateComponent</code>这个方法,这里我们看下这个函数的定义:</p>
<pre><code class="js"> updateComponent = () => {
vm._update(vm._render(), hydrating)
}</code></pre>
<p>简而言之,这时用来渲染页面的,所以当代码执行到<code>this.age = 190</code>,这里就会触发<code>age</code>的<code>setter</code>属性,该属性会调用<code>dep.notify</code>方法:</p>
<pre><code class="js"> // 通知
notify() {
// stabilize the subscriber list first
// 浅拷贝订阅列表
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
// 关闭异步,则subs不在调度中排序
// 为了保证他们能正确的执行,现在就带他们进行排序
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}</code></pre>
<p>这里的<code>this.subs</code>就是页面初始化过程中,<code>age</code>这个属性收集到的依赖关系,也就是<code>renderWatcher</code>实例。接着调用<code>renderWatcher</code>的<code>update</code>方法。</p>
<pre><code class="js"> /**
* Subscriber interface.
* Will be called when a dependency changes.
*/
update() {
// debugger
/* istanbul ignore else */
if (this.lazy) {
// 执行 computedWacher 会运行到这里
this.dirty = true
} else if (this.sync) {
this.run()
} else {
// 运行 renderWatcher
queueWatcher(this)
}
}</code></pre>
<p>那为了更好的理解这里,我把<code>renderWatcher</code>的实例化的代码也贴出来:</p>
<pre><code class="js"> // we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
new Watcher(
vm,
updateComponent,
noop,
{
before() {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
},
true /* isRenderWatcher */
)</code></pre>
<p>因此,<code>renderWatcher</code>是没有设置<code>lazy</code>这个属性的,同时我也没有手动设置<code>sync</code>属性,因此代码会执行到<code>queueWatcher(this)</code>。注意这里的<code>this</code>,当前属于<code>renderWatcher</code>实例对象,因此这里传递的this就是该对象。</p>
<pre><code class="js">// 将一个watcher实例推入队列准备执行
// 如果队列中存在相同的watcher则跳过这个watcher
// 除非队列正在刷新
export function queueWatcher(watcher: Watcher) {
const id = watcher.id
debugger
if (has[id] == null) {
has[id] = true
if (!flushing) {
// 没有在刷新队列,则推入新的watcher实例
queue.push(watcher)
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
// 队列已经刷新,则用传入的watcher实例的id和队列中的id比较,按大小顺序插入队列
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// queue the flush
if (!waiting) {
waiting = true
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
debugger
nextTick(flushSchedulerQueue)
}
}
}</code></pre>
<p>这段代码比较简单,就说一点。代码里有个判断是<code>config.async</code>,这是Vue私有对象上的值,默认的是<code>true</code>,因此代码会执行到<code>nextTick</code>这里,此时会传入一个回调函数<code>flushSchedulerQueue</code>,我们这里先不说,之后用的的时候再介绍。现在看看<code>nextTick</code>的实现。</p>
<pre><code class="js">const callbacks = []
let pending = false
export function nextTick(cb?: Function, ctx?: Object) {
debugger
let _resolve
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
timerFunc()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}</code></pre>
<p><code>pendding</code>用来判断是否存在等待的队列,<code>callbacks</code>是执行回调的队列。那对于此时此刻,就是向<code>callbacks</code>推入一个回调函数,其中要执行的部分就是<code>flushSchedulerQueue</code>。因为是初次调用这个函数,这里的就会调用到<code>timerFunc</code>。</p>
<pre><code class="js"> let timerFunc
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
// In problematic UIWebViews, Promise.then doesn't completely break, but
// it can get stuck in a weird state where callbacks are pushed into the
// microtask queue but the queue isn't being flushed, until the browser
// needs to do some other work, e.g. handle a timer. Therefore we can
// "force" the microtask queue to be flushed by adding an empty timer.
if (isIOS) setTimeout(noop)
}</code></pre>
<p>现在毫无因为的是<code>timerFunc</code>这个函数会被调用。但是有个问题,<code>p.then(flushCallbacks)</code>这句话会执行么?来看个例子:</p>
<pre><code class="js">function callback() {
console.log('callback')
}
let p = Promise.resolve()
function func() {
p.then(callback)
}
console.log('this is start')
func()
console.log('this is pre promise 1')
let a = 1
console.log('this is pre promise 2')
console.log(a)
</code></pre>
<p>思考一下结果是什么吧。看看和答案是否一致:</p>
<p><img src="/img/remote/1460000020412385" alt="" title=""></p>
<p>说回上面,<code>p.then(flushCallbacks)</code>这句话在这里会执行,但是是将<code>flushCallbacks</code>这个方法推入了微任务队列,要等其他的同步代码执行完成,执行栈空了之后才会调用。所以对于<code>renderWatcher</code>来说,目前就算执行完了。</p>
<p>接下来代码执行到这里:</p>
<pre><code class="js">this.$nextTick(() => {
console.log('next tick', this.age)
debugger
})</code></pre>
<p>看下<code>$nextTick</code>的定义:</p>
<pre><code class="js"> Vue.prototype.$nextTick = function (fn: Function) {
return nextTick(fn, this)
}</code></pre>
<p>这里定义<code>$nextTick</code>是定义在Vue的原型对象上,所以在页面中可以通过<code>this.$nextTick</code>调用,同时传入的<code>this</code>就是当前页的实例。所以看会<code>nextTick</code>定义的部分,唯一的区别是,这是的<code>pendding</code>是<code>false</code>,因此不会再调用一次<code>timerFunc</code>。</p>
<p><code>setAge</code>里的同步代码都执行完了,因此就轮到<code>flushCallbacks</code>出场。来看下定义:</p>
<pre><code class="js">function flushCallbacks() {
debugger
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
console.log(copies)
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}</code></pre>
<p>这里定义的位置和定义<code>nextTick</code>是在同一个文件里,因此<code>pendding</code>和<code>callbacks</code>是共享的。主要就看<code>copies[i]()</code>这一段。经过前面的执行,此时<code>callbacks.length</code>的值应该是2。<code>copies[1]</code>指的就是先前推进队列的<code>flushSchedulerQueue</code>。</p>
<pre><code class="js">/**
* Flush both queues and run the watchers.
*
* 刷新队列并且运行watcher
*/
function flushSchedulerQueue() {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id
// Sort queue before flush.
// This ensures that:
// 1. Components are updated from parent to child. (because parent is always
// created before the child)
// 2. A component's user watchers are run before its render watcher (because
// user watchers are created before the render watcher)
// 3. If a component is destroyed during a parent component's watcher run,
// its watchers can be skipped.
// 给刷新队列排序,原因如下:
// 1. 组件的更新是从父组件开始,子组件结束
// 2. 组件的 userWatcher 的运行总是先于 renderWatcher
// 3. 如果父组件的watcher运行期间,子组件被销毁了,后续运行可以跳过被销毁的子组件
queue.sort((a, b) => a.id - b.id)
// do not cache length because more watchers might be pushed
// as we run existing watchers
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
watcher.run()
// in dev build, check and stop circular updates.
if (process.env.NODE_ENV !== 'production' && has[id] != null) {
circular[id] = (circular[id] || 0) + 1
if (circular[id] > MAX_UPDATE_COUNT) {
warn('You may have an infinite update loop ' + (watcher.user ? `in watcher with expression "${watcher.expression}"` : `in a component render function.`), watcher.vm)
break
}
}
}</code></pre>
<p><code>watcher.before</code>这个方法是存在的,先前的代码中有,在初始化<code>renderWatcher</code>时传入了这个参数。这里就调用了<code>callHook(vm, 'beforeUpdate')</code>,所以能看出来,此时<code>beforeUpdate</code>执行了。接着执行<code>watcher.run()</code>。<code>run</code>是<code>Watcher</code>类上定义的一个方法。</p>
<pre><code class="js"> /**
* Scheduler job interface.
* Will be called by the scheduler.
*/
run() {
debugger
if (this.active) {
const value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}</code></pre>
<p><code>this.active</code>初始化的值就是true,<code>get</code>方法之前的文章也提到过,这里再贴一遍代码:</p>
<pre><code class="js"> /**
* Evaluate the getter, and re-collect dependencies.
*/
get() {
// debugger
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}</code></pre>
<p>这部分代码之前说过,这里就不再说了,只提一点,此时的<code>this.getter</code>执行的是<code>updateComponent</code>,其实也就是里面定义的<code>vm._update(vm._render(), hydrating)</code>。关于<code>render</code>和<code>update</code>我会在分析虚拟dom时介绍。</p>
<p>现在需要知道的是,页面此时会重新渲染,我在<code>setAge</code>方法中修改了<code>age</code>的值,当<code>vm._update</code>执行完,就会发现页面上的值变化了。那接着就执行<code>callbacks</code>中的下一个值,也就是我写在<code>$nextTick</code>中的回调函数,这个就很简单,没必要再说。点击按钮到现在新的页面渲染完成,执行的结果就是:</p>
<pre><code>before update
updated
next tick 100</code></pre>
<p>这里就把整个流程讲完了,但是我想到vue文档中说的:</p>
<blockquote>在修改数据之后立即使用它,然后等待 DOM 更新</blockquote>
<p>假设我现在要是把<code>$nextTick</code>放到修改值之前呢。把<code>setAge</code>修改一下。</p>
<pre><code class="js"> setAge() {
this.$nextTick(() => {
console.log('next tick', this.age)
debugger
})
debugger
this.age = 100
}</code></pre>
<p>思考一下,此时点击按钮,页面会打印出什么东西。按照逻辑,因为<code>$nextTick</code>写在了前面,因此会被先推进<code>callbacks</code>中,也就会被第一个执行。所以此时我以为打印出来的<code>age</code>还是2。但我既然都这样说了,那结果肯定是和我以为的不一样,但我有一部分想的没错,就是优先推入,优先调用。当我忘了一点,大家也可以会想一下,<code>renderWatcher</code>是如何被触发的?</p>
<p><code>$nextTick</code>回调现在是进入了微任务队列,所以会继续执行接下来的赋值。此时会触发<code>age</code>设置的<code>setter</code>里的<code>dep.notify</code>。但在调用之前,新的值就已经传给age了。所以当<code>$nextTick</code>里的回调执行时,会触发<code>age</code>的<code>getter</code>,拿到的值就是新的值。</p>
<p>整个<code>nextTick</code>事件就介绍完了。</p>
vue源码分析之计算属性
https://segmentfault.com/a/1190000020380799
2019-09-14T02:08:30+08:00
2019-09-14T02:08:30+08:00
Aria
https://segmentfault.com/u/janesu
3
<p>最近总被问道vue的计算属性原理是什么、计算属性是如何做依赖收集的之类的问题,今天用了一天时间好好研究了下源码,把过程基本捋顺了。总的来说还是比较简单。</p>
<p>先明确一下我们需要弄清楚的知识点:</p>
<ol>
<li>computed属性如何初始化</li>
<li>响应式属性的变化如何引起computed的重新计算</li>
</ol>
<p>弄清楚以上两点后对computed就会有一个比较全面的了解了。</p>
<p>首先,需要弄明白响应式属性是怎么实现的,具体我会在其他文章中写,这里了解个大概就可以。在代码中调用<code>new Vue()</code>的过程实际调用了定义在原型的<code>_init()</code>,在这个方法里会初始化vue的很多属性,这其中就包括建立响应式属性。它会循环定义在<code>data</code>中的所有属性值,通过<code>Object.defineProperty</code>设置每个属性的访问器属性。</p>
<p><img src="/img/remote/1460000020380802" alt="code" title="code"></p>
<p>因此在这个阶段,<code>data</code>中的属性值在获取或者赋值时就能被拦截。紧接着就是初始化computed属性:</p>
<p><img src="/img/remote/1460000020380803" alt="code2" title="code2"></p>
<p>这里要给当前页面实例上新增一个<code>computedWatchers</code>空对象,然后循环<code>computed</code>上的属性。在vue的文档里关于computed介绍,它既可以是函数,也可是是对象,比如下面这种:</p>
<pre><code class="js">new Vue({
computed:{
amount(){
return this.price * this.count
}
}
// 也可以写成下面这种
computed:{
amount:{
get(){
return this.price * this.count
},
set(){}
}
}
})</code></pre>
<p>但因为不建议给computed属性赋值,因此比较常见的都是上面那种。所以在上图的源码中,<code>userDef</code>和<code>getter</code>都是函数。之后就是判断是否是服务端渲染,不是就实例化一个<code>Watcher</code>类。那接着来看一下实例化的这个类是什么。源码太长了我就只展示<code>constructor</code>里的内容。</p>
<pre><code class="js">constructor(vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
// options
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
this.before = options.before
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production' ? expOrFn.toString() : ''
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
process.env.NODE_ENV !== 'production' && warn(`Failed watching path: "${expOrFn}" ` + 'Watcher only accepts simple dot-delimited paths. ' + 'For full control, use a function instead.', vm)
}
}
this.value = this.lazy ? undefined : this.get()
}</code></pre>
<p>在这个阶段做了这么几件事情:</p>
<ol>
<li>向页面实例的<code>watchers</code>属性中依次push了每一个计算属性的实例。</li>
<li>将实例化类时传入的第二个参数(也就是上文提及的<code>getter</code>)设置为<code>this.getter</code>
</li>
<li>
<code>this.value</code>设置为<code>undefined</code>
</li>
</ol>
<p>到这里为止,计算属性的初始化就完成了,如果给生命周期打了断点,你就会发现这些步骤就是在<code>created</code>之前完成的。但是到现在,vue只是创建了响应式属性和把每一个计算属性用watcher实例化,并没有完成计算属性的依赖收集。</p>
<p>紧接着,vue会调用原型上的<code>$mount</code>方法,这里会返回一个函数<code>mountComponent</code>。</p>
<p><img src="/img/remote/1460000020380804" alt="code3" title="code3"></p>
<p>这里关注一下这部分代码:</p>
<pre><code class="js"> // we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
new Watcher(
vm,
updateComponent,
noop,
{
before() {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
},
true /* isRenderWatcher */
)</code></pre>
<p>在挂载阶段,会再次实例化一次<code>Watcher</code>类,但是这里和之前实例的类不一样的地方在于,他的初始化属性<code>isRenderWatcher</code>为true。所以区分一下就是,前文所述的循环计算属性时实例化的<code>Watcher</code>是<code>computedWatcher</code>,而这里的则是<code>renderWatcher</code>。除了从字面上能看出他们之间的区别外。在实例化上也有不同。</p>
<pre><code class="js">// 不同一
if (isRenderWatcher) {
vm._watcher = this
}
// 不同二
this.dirty = this.lazy // for lazy watchers
// 不同三
this.value = this.lazy ? undefined : this.get()</code></pre>
<p><code>renderWatcher</code>会在页面实例上新增一个<code>_watcher</code>属性,并且<code>dirty</code>为false,最重要的是这里会直接调用实例上的方法<code>get()</code></p>
<p><img src="/img/remote/1460000020380805" alt="code0" title="code0"></p>
<p>这块代码就比较重要了,我们一点一点说。</p>
<p><img src="/img/remote/1460000020380806" alt="code01" title="code01"></p>
<p>首先是<code>pushTarget(this)</code>。<code>pushTarget</code>方法是定义在<code>Dep</code>文件里的方法,他的作用是往<code>Dep</code>类的自有属性<code>target</code>上赋值,并且往<code>Dep</code>模块的<code>targetStack</code>数组push当前的<code>Watcher</code>实例。因此对于此时的<code>renderWatcher</code>而言,它的实例被赋值给了<code>Dep</code>类上的属性。</p>
<p>接下来就是调用当前<code>renderWatcher</code>实例的getter方法,也就是上面代码中提到的<code>updateComponent</code>方法。</p>
<pre><code class="js">updateComponent = () => {
vm._update(vm._render(), hydrating)
}</code></pre>
<p>这里涉及到虚拟dom的部分,我不在这里详说,以后会再分析。因此现在对于页面来说,就是将vue中定义的所有data,props,methods,computed等挂载在页面上。为了页面正常显示,当然是需要获取值的,上文中所说的为data的每个属性设置getter访问器属性,这里就能用到。再看下getter的代码:</p>
<pre><code class="js">get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
}</code></pre>
<p><code>Dep.target</code>上现在是有值的,就是<code>renderWatcher</code>实例,<code>dep.depend</code>就能被顺利调用。来看下<code>dep.depend</code>的代码:</p>
<pre><code class="js"> depend() {
if (Dep.target) {
Dep.target.addDep(this)
}
}</code></pre>
<p>这里调用了<code>renderWatcher</code>实例上的<code>addDep</code>方法:</p>
<pre><code class="js"> /**
* Add a dependency to this directive.
*/
addDep(dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}</code></pre>
<p>代码看起来可能不是很清晰,实际上这里做了三件事:</p>
<ol>
<li>如果该<code>renderWatcher</code>实例的<code>newDepIds</code>属性不存在当前正在处理的data属性的id,则添加</li>
<li>将当前data属性的<code>Dep</code>实例添加到<code>renderWatcher</code>的<code>newDeps</code>属性中</li>
<li>调用当前data属性的<code>Dep</code>实例上的方法<code>dep.addSub</code>
</li>
</ol>
<pre><code class="js"> // 添加订阅
addSub(sub: Watcher) {
this.subs.push(sub)
}</code></pre>
<p>所以第三步就是在做依赖收集的工作。对于这里,就是为每一个响应式属性添加了<code>updateComponent</code>依赖,这样修改响应式属性的值就能够引起页面的重新渲染,也就是<code>vnode</code>的<code>patch</code>过程。</p>
<p>相应的,<code>computed</code>属性也会被渲染在页面上而被调用,和data属性的原理一样,<code>computed</code>也有访问器属性的设置,在第二张图中,调到的<code>defineComputed</code>方法:</p>
<pre><code class="js">export function defineComputed(target: any, key: string, userDef: Object | Function) {
const shouldCache = !isServerRendering()
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = shouldCache ? createComputedGetter(key) : createGetterInvoker(userDef)
sharedPropertyDefinition.set = noop
} else {
sharedPropertyDefinition.get = userDef.get ? (shouldCache && userDef.cache !== false ? createComputedGetter(key) : createGetterInvoker(userDef.get)) : noop
sharedPropertyDefinition.set = userDef.set || noop
}
if (process.env.NODE_ENV !== 'production' && sharedPropertyDefinition.set === noop) {
sharedPropertyDefinition.set = function() {
warn(`Computed property "${key}" was assigned to but it has no setter.`, this)
}
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}</code></pre>
<p><code>sharedPropertyDefinition</code>是一个通用的访问器对象:</p>
<pre><code class="js">const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}</code></pre>
<p>因此当调用计算属性的时候,就是在调用计算属性上绑定的函数。这里在给<code>get</code>赋值时调用了另一个函数<code>createComputedGetter</code>。</p>
<pre><code class="js">function createComputedGetter(key) {
return function computedGetter() {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}</code></pre>
<p>这部分代码做的事情就很有意思了,和<code>renderWatcher</code>调用<code>get</code>做的类似,<code>watcher.evaluate</code>方法会间接调用<code>computedWatcher</code>的<code>get</code>方法,然后调用计算属性上的函数,因为计算属性会根据不同的响应式属性而返回值,调用每一个响应式属性都会触发getter,因此和计算属性相关的响应式属性的<code>Dep</code>实例上会订阅计算属性的变化。</p>
<p>说到这,计算属性的依赖收集就做完了。在这之后如果修改了某一个和计算属性绑定的响应式属性,就会触发<code>setter</code>:</p>
<pre><code class="js"> set: function reactiveSetter(newVal) {
// 获取旧属性值
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// #7981: for accessor properties without setter
// 用于没有setter的访问器属性
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify() // 注意这里
}</code></pre>
<p>这里会调用<code>dep.notify</code>:</p>
<pre><code class="js"> // 通知
notify() {
// stabilize the subscriber list first
// 浅拷贝订阅列表
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
// 关闭异步,则subs不在调度中排序
// 为了保证他们能正确的执行,现在就带他们进行排序
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}</code></pre>
<pre><code class="js"> /**
* Subscriber interface.
* Will be called when a dependency changes.
*/
update() {
debugger
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}</code></pre>
<p>对于计算属性,会重复上面的逻辑,直到新的页面渲染完成。</p>
用typescript开发koa2的二三事
https://segmentfault.com/a/1190000016988818
2018-11-12T21:34:16+08:00
2018-11-12T21:34:16+08:00
Aria
https://segmentfault.com/u/janesu
15
<h2>前言</h2>
<p>最近在写一个博客的项目,前端用的 <code>vue+typescript+element-ui</code>,后台则选择了 <code>koa2+typescript+mongoDB</code>的组合。写这篇博客的目的也是在写后台的过程遇到一些问题,查了很多资料才解决。于是权当总结,亦是记录,可以给别人做一个完整的参考。</p>
<h2>基本信息</h2>
<p>这里列出来的是会用到的一些配置信息,毕竟一直都在更新,可能这里说的以后某个版本就不支持了。</p>
<pre><code>"nodemon" : "^1.18.3",
"ts-node" : "^7.0.1",
"typescript" : "^3.1.1"
"node" : "9.0.0"</code></pre>
<h2>问题描述</h2>
<p>这次遇到的问题其实都和typescript有关。koa2已经出来很久了,开发基本成熟,但是这次找资料的时候鲜有发现使用typescript开发的,即便有,也都很简单,而且没法解决我的问题。</p>
<p>那言归正传,使用ts开发koa,因为不涉及webpack打包编译,所以就会遇到几个问题:</p>
<blockquote><ol>
<li>编译</li>
<li>实时刷新,重启服务器</li>
<li>debugger</li>
</ol></blockquote>
<p>这些确实是初期很困扰我的地方,使用node开发,最简单的无非是 <code>node xxx.js</code>,进一步也就是热更新。但引入ts后就需要考虑编译和实时刷新的问题。毕竟不像每改一点代码,就手动重启服务器,手动编译。</p>
<h2>解决方案</h2>
<p>以下是我的解决方案,后面我会说一下为什么这样写,如果来不及看或者只想要答案的话复制就行。</p>
<pre><code>"watch" : "ts-node ./app/index.ts",
"start" : "nodemon --watch app/index.js",
"build" : "tsc",
"debugger" : "nodemon --watch ./app -e ts,tsx --exec node --inspect -r ts-node/register ./app/index.ts",
"watch-serve": "nodemon --watch './app/**/*' -e ts,tsx --exec ts-node ./app/index.ts"</code></pre>
<p>那我们一个一个来说。</p>
<h3><code>npm run watch</code></h3>
<p><img src="/img/bVbjrzg?w=1341&h=826" alt="clipboard.png" title="clipboard.png"></p>
<p>这个命令就是在本地使用<code>ts-node</code>启动一个服务器。来看一下对<code>ts-node</code>的描述。</p>
<blockquote>TypeScript execution and REPL for node.js, with source map support. Works with typescript@>=2.0.</blockquote>
<p>这是一个在<code>node.js</code>的执行和交互的typescript环境,简而言之就是为了ts而生的!!</p>
<p>那这条命令就是根据当前的入口运行程序,唯一的一个问题是,不支持热更新。所以pass。</p>
<h3>
<code>npm run build</code> && <code>npm run start</code>
</h3>
<p>这俩放一起说是因为相关性比较高。可以说是相互依赖的关系吧。</p>
<p>先说第一条命令,很简单,就是编译当前的ts项目文件,输出目录需要在<code>tsconfig.json</code>中配置。我给大家看下我的运行结果。</p>
<p><img src="/img/bVbjrAn?w=386&h=247" alt="clipboard.png" title="clipboard.png"></p>
<p><code>app</code>是我的项目文件,运行命令后,会在根目录下创建<code>dist</code>文件夹存放我编译好的js文件,打开就是这样。</p>
<p><img src="/img/bVbjrAx?w=374&h=363" alt="clipboard.png" title="clipboard.png"></p>
<p>现在再说第二条命令,就是根据编译好的文件入口启动服务器。并且支持热更新,但是,<code>注意这里有个但是</code>,它只支持编译过后的文件的热更新,其实就是用js开发koa的启动命令,那这时候在源文件中的任何修改都不会有作用,所以pass。</p>
<h3><code>npm run watch-serve </code></h3>
<p>重点来了,这才是解决问题的关键!!!</p>
<p><img src="/img/bVbjrA5?w=2683&h=1652" alt="clipboard.png" title="clipboard.png"></p>
<p>这里完美的解决了<strong>代码的热更新,实时编译,服务器重启</strong>等问题。很好的提升了开发体验。</p>
<p>这个解决方案有一些中文博客提到,但是当初用的时候不知道为啥这样用,导致后期犯了一些现在看来很低级的错误,这个就不提了。不过确实没人说明这段命令的意思,直到昨天碰到一个问题,我才好好正视这个恶魔。</p>
<p><code>nodemon</code>和<code>ts-node</code>前文都介绍过了,我在这里只会针对具体的配置解释一下。原本我的理解是这里用逗号分隔了两个不同的命令,但是我太天真了。来看一下文档的介绍。</p>
<blockquote>By default, nodemon looks for files with the .js, .mjs, .coffee, .litcoffee, and .json extensions. If you use the --exec option and monitor app.py nodemon will monitor files with the extension of .py. However, you can specify your own list with the -e (or --ext) switch like so:</blockquote>
<pre><code>nodemon -e js,jade</code></pre>
<blockquote>Now nodemon will restart on any changes to files in the directory (or subdirectories) with the extensions .js, .jade.</blockquote>
<p><code>nodemon</code>有默认吃的几种文件类型,分别是 <code>.js, .mjs, .coffee, .litcoffee, and .json</code>,而我这里用的 <code>.ts</code>,并不在默认支持文件里,因此这里使用 <code>-e</code>来指定我需要扩展的文件类型,这里的逗号也不过是用来分隔不同类型用的。那这里提到了 <code>--exec</code>这个配置。原文里说如果用<code>nodemon</code>启动<code>app.py</code>这个文件,那么将默认支持<code>.py</code>这种扩展类型。另外文档里还写了别的。</p>
<blockquote>nodemon can also be used to execute and monitor other programs. nodemon will read the file extension of the script being run and monitor that extension instead of .js if there's no nodemon.json:</blockquote>
<pre><code>nodemon --exec "python -v" ./app.py</code></pre>
<blockquote>Now nodemon will run app.py with python in verbose mode (note that if you're not passing args to the exec program, you don't need the quotes), and look for new or modified files with the .py extension.</blockquote>
<p>这里说明,除了默认支持的扩展,通过这个配置,可以支持和正在运行的脚本一样的扩展。并且,如果扩展程序不需要传参数的话,可以不写单引号。</p>
<p>综上所述,一个命令用于增加支持的文件类型,一个配置用来执行和监视其他类型的程序。</p>
<p>至于<code>---watch</code>这个参数。</p>
<blockquote>By default nodemon monitors the current working directory. If you want to take control of that option, use the --watch option to add specific paths:<p><code>nodemon --watch app --watch libs app/server.js</code></p>
<p>Now nodemon will only restart if there are changes in the ./app or ./libs directory. By default nodemon will traverse sub-directories, so there's no need in explicitly including sub-directories.</p>
<p>Don't use unix globbing to pass multiple directories, e.g --watch ./lib/*, it won't work. You need a --watch flag per directory watched.</p>
</blockquote>
<p>这里面需要注意的有两点,一是<code>nodemon</code>会默认监视当前脚本文件执行的文件夹,另一个就是如果要指定具体的文件夹时,需要些详细的路径,比如绝对路径或者相对路径,绝对不要使用<strong>通配符</strong>。因此我命令行中的使用是无效且违反规则的,然而非要这样写也不影响运行。</p>
<p>原本到这也就结束了,然而昨天用了一个npm包,我想看看怎么运行的,于是遇到了<code>debugger</code>的问题,这也是迫使我去认真弄懂这段命令的原因。</p>
<h3><code>npm run debugger</code></h3>
<p>基本的调试方式网上到处都有,我就不说了,问题还是导入typescript之后,让一切都混乱起来。我最开始尝试了以下几种命令:</p>
<pre><code>'nodemon --inspect --watch ./app -e ts,tsx --exec ts-node ./app/index.ts'
'nodemon --watch --inspect ./app -e ts,tsx --exec ts-node ./app/index.ts'
'nodemon --watch ./app -e ts,tsx --exec ts-node --inspect ./app/index.ts'</code></pre>
<p>这些都可以自己试着运行一下,反正也没啥用。然后就是今天一直想着这件事,换了几个关键字google,找到这两个地方。</p>
<blockquote>
<a href="https://link.segmentfault.com/?enc=tEBOvdqeY8epV5V2fSM%2BAA%3D%3D.TMy66cgvPSQVaYrjID7iJ0MDeuPXR3J7830KDfkgDn0y4x4Ogh8Bre%2BCb1tnTWlviFOERo169mvrf0AXoB3GJDmXurGg8t1qVdssnb0qdyepl%2BUzDDnxqkzdQytDeMO9ufkTNr5KG0sTrXd%2FSkHfXaPNFYkyx77FDXB9ZgYWetE%3D" rel="nofollow">https://stackoverflow.com/que...</a><p><a href="https://link.segmentfault.com/?enc=00hAbvUIRHIovc%2Fdv3hfmA%3D%3D.exRtOEzGZ4rLLUpm27HfZBAay39tuz5zut9xv%2B0jS9SkQGBn1V0%2BCUSqDGh9%2B3sWijo8ArTcXskSslDFEuHJcQ%3D%3D" rel="nofollow">https://github.com/TypeStrong...</a></p>
</blockquote>
<p>感谢stackoverflow和github,相互印证着看好像就明白是怎么回事了。</p>
<p>这里说下<code>-r</code>这个参数:</p>
<p><img src="/img/bVbjrIB?w=590&h=112" alt="clipboard.png" title="clipboard.png"></p>
<p>这里用于预加载一个模块,并且可以多次使用这个参数,那说回我写的命令里,<code>ts-node/register</code> 就是一个模块,或者不严谨的说,<code>register</code>是<code>ts-node</code>下的一个方法。这里就是使用node预加载ts-node的register模块用来运行ts程序,并且开启debugger模式。</p>
<h2>后语</h2>
<p>至此为止,在编译,热更新,debugger方面的坑应该是踩完了,希望后面的人看了我写的文章能少走些弯路吧。如果有些的不对的地方可以留言指正。所有的源码我应该暂时不会放出,至少等我写完吧。</p>
<p>就酱紫,</p>
简单的例子实现vue插件
https://segmentfault.com/a/1190000012224638
2017-11-30T09:58:26+08:00
2017-11-30T09:58:26+08:00
Aria
https://segmentfault.com/u/janesu
36
<p>一直都觉得vue的插件生涩难懂,但是又很好奇,在看了几篇文章,试着写了写之后觉得也没那么难,这篇文就是总结一下这个过程,加深记忆,也可以帮助后来的人。</p>
<h2>why</h2>
<p>在学习之前,先问问自己,为什么要编写vue的插件。</p>
<p>在一个项目中,尤其是大型项目,有很多部分需要复用,比如加载的loading动画,弹出框。如果一个一个的引用也稍显麻烦,而且在一个vue文件中引用的组件多了,会显得代码臃肿,所以才有了封装vue插件的需求。</p>
<p>说完需求,就来看看具体实现。目前我尝试了两种不一样的插件编写的方法,逐个介绍。</p>
<p><img src="/img/remote/1460000012224643?w=704&h=530" alt="" title=""></p>
<p>这是我的项目目录,大致的结构解释这样,尽量简单,容易理解。</p>
<p>一个是loading插件,一个是toast插件,不同的地方在于:loading插件是作为组件引入使用,而toast插件是直接添加在挂载点里,通过方法改变状态调用的。</p>
<p>目前使用起来是酱紫的:</p>
<p><img src="/img/remote/1460000012224644?w=672&h=720" alt="" title=""></p>
<h2>toast插件</h2>
<p>toast文件下有两个文件,后缀为vue的文件就是这个插件的骨架,js文件一个是将这个骨架放入Vue全局中,并写明操作逻辑。</p>
<p>可以看一下toast.vue的内容:</p>
<pre><code class="js"><template>
<transition name="fade">
<div class="toast" v-show="show">
{{message}}
</div>
</transition>
</template>
<script>
export default {
data() {
return {
show: false,
message: ""
};
}
};
</script>
<style lang="scss" scoped>
.toast {
position: fixed;
top: 40%;
left: 50%;
margin-left: -15vw;
padding: 2vw;
width: 30vw;
font-size: 4vw;
color: #fff;
text-align: center;
background-color: rgba(0, 0, 0, 0.8);
border-radius: 5vw;
z-index: 999;
}
.fade-enter-active,
.fade-leave-active {
transition: 0.3s ease-out;
}
.fade-enter {
opacity: 0;
transform: scale(1.2);
}
.fade-leave-to {
opacity: 0;
transform: scale(0.8);
}
</style></code></pre>
<p>这里面主要的内容只有两个,决定是否显示的<code>show</code>和显示什么内容的<code>message</code>。</p>
<p>粗看这里,有没有发现什么问题?</p>
<p>这个文件中并没有<code>props</code>属性,也就是无论是show也好,message也好,就没有办法通过父子组件通信的方式进行修改,那他们是怎么正确处理的呢。别急,来看他的配置文件。</p>
<p>index.js:</p>
<pre><code class="js">import ToastComponent from './toast.vue'
const Toast = {};
// 注册Toast
Toast.install = function (Vue) {
// 生成一个Vue的子类
// 同时这个子类也就是组件
const ToastConstructor = Vue.extend(ToastComponent)
// 生成一个该子类的实例
const instance = new ToastConstructor();
// 将这个实例挂载在我创建的div上
// 并将此div加入全局挂载点内部
instance.$mount(document.createElement('div'))
document.body.appendChild(instance.$el)
// 通过Vue的原型注册一个方法
// 让所有实例共享这个方法
Vue.prototype.$toast = (msg, duration = 2000) => {
instance.message = msg;
instance.show = true;
setTimeout(() => {
instance.show = false;
}, duration);
}
}
export default Toast</code></pre>
<p>这里的逻辑大致可以分成这么几步:</p>
<ol>
<li>创建一个空对象,这个对象就是日后要使用到的插件的名字。此外,这个对象中要有一个install的函数。</li>
<li>使用vue的extend方法创建一个插件的构造函数(可以看做创建了一个vue的子类),实例化该子类,之后的所有操作都可以通过这个子类完成。</li>
<li>之后再Vue的原型上添加一个共用的方法。</li>
</ol>
<p>这里需要着重提的是<code>Vue.extend()</code>。举个例子,我们日常使用vue编写组件是这个样子的:</p>
<pre><code class="js">Vue.component('MyComponent',{
template:'<div>这是组件</div>'
})</code></pre>
<p>这是全局组件的注册方法,但其实这是一个语法糖,真正的运行过程是这样的:</p>
<pre><code class="js">let component = Vue.extend({
template:'<div>这是组件</div>'
})
Vue.component('MyComponent',component)</code></pre>
<p>Vue.extend会返回一个对象,按照大多数资料上提及的,也可以说是返回一个Vue的子类,既然是子类,就没有办法直接通过他使用Vue原型上的方法,所以需要new一个实例出来使用。</p>
<pre><code>在代码里console.log(instance)
</code></pre>
<p>得出的是这样的结果:</p>
<p><img src="/img/remote/1460000012224645?w=1134&h=1202" alt="" title=""></p>
<p>可以看到</p>
<pre><code>
$el:div.toast
</code></pre>
<p>也就是toast组件模板的根节点。</p>
<p>疑惑的是,我不知道为什么要创建一个空的div节点,并把这个实例挂载在上面。我尝试注释这段代码,但是运行会报错。</p>
<p><img src="/img/remote/1460000012224646?w=852&h=370" alt="" title=""></p>
<p>查找这个错误的原因,貌似是因为</p>
<pre><code> document.body.appendChild(instance.$el)
</code></pre>
<p>这里面的<code>instance.$el</code>的问题,那好,我们console下这个看看。WTF!!!!结果居然是<code>undefined</code>。</p>
<p>那接着</p>
<pre><code>console.log(instance)
</code></pre>
<p><img src="/img/remote/1460000012224647?w=1096&h=978" alt="" title=""></p>
<p>和上一张图片比对一下,发现了什么?对,$el消失了,换句话说在我注释了</p>
<pre><code>instance.$mount(document.createElement('div'))
</code></pre>
<p>这句话之后,挂载点也不存在了。接着我试着改了一下这句:</p>
<pre><code>instance.$mount(instance.$el)
</code></pre>
<p><img src="/img/remote/1460000012224648" alt="" title=""></p>
<p>$el又神奇的回来了………………</p>
<p>暂时没有发现这种改动有什么问题,可以和上面一样运行。但无论如何,这也就是说instance实例必须挂载在一个节点上才能进行后续操作。</p>
<p>之后的代码就简单了,无非是在Vue的原型上添加一个改变插件状态的方法。之后导出这个对象。</p>
<p>接下来就是怎么使用的问题了。来看看main.js是怎么写的:</p>
<pre><code class="js">import Vue from 'vue'
import App from './App'
// import router from './router'
import Toast from './components/taost'
Vue.use(Toast)
Vue.config.productionTip = false
/* eslint-disable no-new */
new Vue({
// router,
render: h => h(App)
}).$mount('#app')
</code></pre>
<p>这样就可以在其他vue文件中直接使用了,像这样:</p>
<pre><code class="js">// app.vue
<template>
<div id="app">
<loading duration='2s' :isshow='show'></loading>
<!-- <button @click="show = !show">显示/隐藏loading</button> -->
<button @click="toast">显示taost弹出框</button>
</div>
</template>
<script>
export default {
name: "app",
data() {
return {
show: false
};
},
methods: {
toast() {
this.$toast("你好");
}
}
};
</script>
<style>
#app {
font-family: "Avenir", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style></code></pre>
<p>通过在methods中增加一个方法控制写在Vue原型上的$toast对toast组件进行操作。</p>
<p>这样toast组件的编写过程就结束了,可以看到一开始gif图里的效果。</p>
<h2>loading插件</h2>
<p>经过上一个插件的讲解,这一部分就不会那么细致了,毕竟大多数都没有什么不同,我只指出不一样的地方。</p>
<pre><code class="js"><template>
<div class='wrapper' v-if="isshow">
<div class='loading'>
<img src="./loading.gif" alt="" width="40" height="40">
</div>
</div>
</template>
<script>
export default {
props: {
duration: {
type: String,
default: "1s" //默认1s
},
isshow: {
type: Boolean,
default: false
}
},
data: function() {
return {};
}
};
</script>
<style lang="scss" scoped>
</style></code></pre>
<p>这个就只是一个模板,传入两个父组件的数据控制显示效果。</p>
<p>那再来看一下该插件的配置文件:</p>
<pre><code class="js">import LoadingComponent from './loading.vue'
let Loading = {};
Loading.install = (Vue) => {
Vue.component('loading', LoadingComponent)
}
export default Loading;</code></pre>
<p>这个和taoat的插件相比,简单了很多,依然是一个空对象,里面有一个install方法,然后在全局注册了一个组件。</p>
<h2>比较</h2>
<p>那介绍了这两种不同的插件编写方法,貌似没有什么不一样啊,真的是这样么?</p>
<p>来看一下完整的main.js和app.vue这两个文件:</p>
<pre><code class="js">// main.js
import Vue from 'vue'
import App from './App'
// import router from './router'
import Toast from './components/taost'
import Loading from './components/loading'
Vue.use(Toast)
Vue.use(Loading)
Vue.config.productionTip = false
/* eslint-disable no-new */
new Vue({
// router,
render: h => h(App)
}).$mount('#app')
</code></pre>
<pre><code class="js">// app.vue
<template>
<div id="app">
<loading duration='2s' :isshow='show'></loading>
<!-- <button @click="show = !show">显示/隐藏loading</button> -->
<button @click="toast">显示taost弹出框</button>
</div>
</template>
<script>
export default {
name: "app",
data() {
return {
show: false
};
},
methods: {
toast() {
this.$toast("你好");
}
}
};
</script>
<style>
#app {
font-family: "Avenir", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style></code></pre>
<p>可以看出来,loading是显示的写在app.vue模板里的,而toast并没有作为一个组件写入,仅仅是通过一个方法控制显示。</p>
<p>来看一下html结构和vue工具给出的结构:</p>
<p><img src="/img/remote/1460000012224649?w=906&h=406" alt="" title=""></p>
<p><img src="/img/remote/1460000012224650?w=414&h=154" alt="" title=""></p>
<p>看出来了么,toast插件没有在挂载点里面,而是独立存在的,也就是说当执行</p>
<pre><code>vue.use(toast)
</code></pre>
<p>之后,该插件就是生成好的了,之后的所有操作无非就是显示或者隐藏的问题了。</p>
用一个简单的例子弄懂vuex和模块化
https://segmentfault.com/a/1190000012083258
2017-11-20T17:24:52+08:00
2017-11-20T17:24:52+08:00
Aria
https://segmentfault.com/u/janesu
6
<p>这篇文章预设你已经了解vue相关的基础知识,因此本文不再赘述。</p>
<p>对vuex的定位和解释可以看官方文档,说的很详细了,我主要从实用的角度写一写如何在实际项目中使用vuex,例子真的很简单(简陋),但是主要是理解这种思维就好。</p>
<p>例子是在vue-cli基础上构建的,以下是src文件下的内容目录。</p>
<pre><code>├── App.vue
├── components // 组件文件夹
│ ├── tab1.vue
│ ├── tab2.vue
│ ├── tab3.vue
│ └── tab4.vue
├── main.js // vue的主文件入口
├── router // vue-router文件
│ └── index.js
└── store // vuex文件
├── action.js // action
├── getter.js // getter
├── index.js // vuex的主文件
├── module // 模块文件
│ ├── tab2.js
│ └── tab3.js
├── mutation-type.js // mutation常量名文件
└── mutation.js // mutation</code></pre>
<p>效果是这样的(不要嫌弃简陋啊啊啊)</p>
<p><img src="/img/remote/1460000012083263?w=1410&h=522" alt="" title=""></p>
<p>在这个例子里,把文档里提到的vuex的相关知识都使用了一遍,包括模块相关的知识,基本把一般的使用场景都覆盖了吧。</p>
<p>那不废话了,开始吧。</p>
<p>首先app.vue和router两部分是和路由相关,就是很简单的东西,看看文档就能了解。</p>
<h2>vuex的模块化</h2>
<p>在写这个例子之前看了很多的开源项目的代码,一开始蛮新鲜的,毕竟之前项目中并没有深度使用过vuex,基本就是一个store.js里把vuex的功能就都完成了,但是项目复杂肯定不能这么写,正好现在有这个需求,我就想写个例子理一理这方面的思路。结果还是蛮简单的。</p>
<p>store文件里的内容就是按照vuex五个核心概念建立的,这么做的好处对于梳理业务逻辑和后期维护都是极大的方便,比如<code>mutation.js</code>和<code>mutation-type.js</code>这两个文件:</p>
<pre><code class="js">// mutation-type.js
const CHANGE_COUNT = 'CHANGE_COUNT';
export default {
CHANGE_COUNT
}
</code></pre>
<pre><code class="js">// mutation.js
import type from './mutation-type'
let mutations = {
[type.CHANGE_COUNT](state) {
state.count++
}
}
export default mutations</code></pre>
<p>将mutation中的方法名单独作为常量提取出来,放在单独的文件中,用的时候引用相关的内容,这样非常便于管理和了解有哪些方法存在,很直观。另一方面,有时候可能需要用到action,可以使用相同的方法名,只要再引入常量的文件就行。</p>
<pre><code class="js">// action.js
import type from './mutation-type'
let actions = {
[type.CHANGE_COUNT]({ commit }) {
commit(type.CHANGE_COUNT)
}
}
export default actions</code></pre>
<p>怎么样,这样是不是看起来就没有写在一个文件里那么乱了。</p>
<h2>...mapGetters和...mapActions</h2>
<p>tab1.vue里:</p>
<pre><code class="js">// tab1.vue
<template>
<div>
<p>这是tab1的内容</p>
<em @click="add">{{count}}</em>
<p>getter:{{NewArr}}</p>
</div>
</template>
<script>
import { mapActions, mapGetters } from "vuex";
import type from "../store/mutation-type";
export default {
computed: {
...mapGetters([
'NewArr'
]),
count: function() {
return this.$store.state.count;
},
},
methods: {
...mapActions({
CHANGE_COUNT: type.CHANGE_COUNT
}),
add: function() {
this.CHANGE_COUNT(type.CHANGE_COUNT);
}
}
};
</script>
<style lang="" scoped>
</style>
</code></pre>
<p>index.js文件里:</p>
<pre><code class="js">import Vuex from 'vuex'
import Vue from 'vue'
import actions from './action'
import mutations from './mutation'
import getters from './getter'
import tab2 from './module/tab2'
import tab3 from './module/tab3'
Vue.use(Vuex)
let state = {
count: 1,
arr:[]
}
let store = new Vuex.Store({
state,
getters,
mutations,
actions,
modules:{
tab2,tab3
}
})
export default store
</code></pre>
<p>vuex提供了一种叫做<strong>辅助函数</strong>的东西,他的好处能让你在一个页面集中展示一些需要用到的东西,并且在使用的时候也可以少写一些内容,不过这个不是必须,根据自己需要取用。</p>
<p>需要说明的是,他们两个生效的地方可不一样。</p>
<p>...mapGetters写在本页面的计算属性中,之后就可以像使用计算属性一样使用getters里的内容了。</p>
<p>...mapActions写在本页面的methods里面,既可以在其他方法里调用,甚至可以直接写在@click里面,像这样:</p>
<pre><code><em @click="CHANGE_COUNT">{{count}}</em>
</code></pre>
<p>酱紫,tab1里面的数字每次点击都会自增1。</p>
<h2>mudule</h2>
<p>vuex的文档里对于模块这部分写的比较模糊,还是得自己实际使用才能行。</p>
<p>在本例子中,我设置了两个模块:tab2和tab3,分别对应着同名的两个组件,当然,我这样只是为了测试,实际看tab2就可以。</p>
<pre><code class="js">// module/tab2.js
const tab2 = {
state: {
name:`这是tab2模块的内容`
},
mutations:{
change2(state){
state.name = `我修改了tab2模块的内容`
}
},
getters:{
name(state,getters,rootState){
console.log(rootState)
return state.name + ',使用getters修改'
}
}
}
export default tab2;</code></pre>
<pre><code class="js">// tab2.vue
<template>
<div>
<p>这是tab2的内容</p>
<strong @click="change">点击使用muttion修改模块tab2的内容:{{info}}</strong>
<h4>{{newInfo}}</h4>
</div>
</template>
<script>
export default {
mounted() {
// console.log(this.$store.commit('change2'))
},
computed: {
info: function() {
return this.$store.state.tab2.name;
},
newInfo(){
return this.$store.getters.name;
}
},
methods: {
change() {
this.$store.commit('change2')
}
}
};
</script>
<style lang="" scoped>
</style>
</code></pre>
<p>这个例子里主要是看怎么在页面中调用模块中的stated等。</p>
<p>首先说state,这个很简单,在页面中这样写就行:</p>
<pre><code>this.$store.steta.tab2(模块名).name
</code></pre>
<p>在本页面的mounted中console一下$store是这样的:</p>
<p><img alt="" title="" src=""></p>
<p>可以看到模块中的stete加了一层嵌套在state中的。</p>
<p>至于<code>action</code>,<code>mutation</code>,<code>getter</code>,和一般的使用方法一样,没有任何区别。</p>
<p>还有就是,在getter和action中,可以通过rootState获得根结构的state,mutation中没有此方法。</p>
underscore源码解读之debounce
https://segmentfault.com/a/1190000011785670
2017-10-31T01:18:03+08:00
2017-10-31T01:18:03+08:00
Aria
https://segmentfault.com/u/janesu
2
<p>刚写完一篇debounce(防抖)函数的实现,我又看了下underscore.js的实现方法。算是趁热打铁,分析一下underscore里实现的套路。</p>
<p>先贴上源码:</p>
<pre><code class="js">_.debounce = function(func, wait, immediate) {
var timeout, args, context, timestamp, result;
var later = function() {
var last = _.now() - timestamp;
console.log(last)
if (last < wait && last >= 0) {
console.log(1)
timeout = setTimeout(later, wait - last);
} else {
console.log(2)
timeout = null;
if (!immediate) {
result = func.apply(context, args);
if (!timeout) context = args = null;
}
}
};
return function() {
context = this;
args = arguments;
timestamp = _.now();
var callNow = immediate && !timeout;
console.log(timeout)
if (!timeout) timeout = setTimeout(later, wait);
if (callNow) {
result = func.apply(context, args);
context = args = null;
}
return result;
};
};</code></pre>
<p>一看可能有点多,我简化一下,整体其实就两部分:</p>
<pre><code class="js">_.debounce = function( func, wait, immediate){
// 函数的回调部分
// 当immediate === false时
// func真正的执行部分
function later(){};
return function(){
// 在这里判断func是否立即执行
// 是否有计时器的存在
}
}</code></pre>
<p>上一篇文章已经分析过this的指向和event的传递,这里就不多说了。直接来分析返回的匿名函数部分。</p>
<pre><code class="js">return function() {
context = this;
args = arguments;
// 这里调用了underscore封装的调用时间戳的方法
// 等同于
// timestamp = Date.now()
timestamp = _.now();
var callNow = immediate && !timeout;
console.log(timeout)
if (!timeout) timeout = setTimeout(later, wait);
if (callNow) {
result = func.apply(context, args);
context = args = null;
}
};</code></pre>
<p>这里我要说的是timestamp,它存储的是动作发生时的时间戳,假设我这里调用debounce时传入的wait为10000,也就是10秒。我第一次调用事件函数是在10:00:00,按照设定,10:00:10之后才能调用第二次方法,在这10秒内,任何调用都是不执行的。</p>
<p>当我第一次执行事件时</p>
<pre><code>timeout = undefined;
immediate先设置为false
</code></pre>
<p>所以</p>
<pre><code>callNow === false
</code></pre>
<p>只有这句话是执行的</p>
<pre><code>if (!timeout) timeout = setTimeout(later, wait);
</code></pre>
<p>那接着来看later都有什么:</p>
<pre><code class="js">var later = function() {
// var last = Date.now() - timestamp;
var last = _.now() - timestamp;
console.log(last)
if (last < wait && last >= 0) {
console.log(1)
timeout = setTimeout(later, wait - last);
} else {
console.log(2)
timeout = null;
if (!immediate) {
result = func.apply(context, args);
if (!timeout) context = args = null;
}
}
};</code></pre>
<p>在上一篇中,判断wait内重复输入,我们取消事件的方法是这样的</p>
<pre><code>if(timer){clearTimeout(timer)}
</code></pre>
<p>但在这里,我们是不是都还没看到怎么处理wait时间内,重复输入无效的问题?别急,现在就来说。玄妙都在这个last变量上。</p>
<p>之前说过,timestamp存储的是第一次事件执行时的时间戳(10:00:00),但现在我没想等十秒,过了五秒我就触发了第二次事件。所以timestamp现在的内容就变成新的时间戳了(10:00:05)。但问题是,timer的回调函数至少要到10:00:10之后才会执行,也就是说</p>
<pre><code>last>=5
</code></pre>
<p>由于代码执行堵塞导致last>10的情况有可能存在,但是不符合我们现在讨论的,而且真的是太特殊了,我们就不说了。那就假设last为5秒(5000ms)。</p>
<pre><code>last < wait && last >= 0
</code></pre>
<p>这句话就是true,那就执行里面的代码。但注意看里面计时器对于时间的写法。</p>
<pre><code>wait - last
</code></pre>
<p>换个说法就是,你在10:00:00启动了我,但是你在10:00:05又动了,我原本应该在10:00:10执行,但是现在惩罚你提前行动,那你之前等的时间就不算,你要再重新多等这几秒10:00:15。</p>
<p>这个难点解决了,其他就都好说。</p>
<p>lster剩余的部分就是判断如果当初设置的是立即执行(immediate = true),func就不再执行一遍了,否则(immediate = false)func执行。</p>
<p>恩,那这个的解读就结束了,有什么地方我没写清楚的话,请给我留言。</p>
Express系列之multer
https://segmentfault.com/a/1190000011740828
2017-10-27T01:23:06+08:00
2017-10-27T01:23:06+08:00
Aria
https://segmentfault.com/u/janesu
1
<p>这两天在看《nodejs权威指南》,这本书看了好久了,但是读的一直不细,这次才好好看了一遍。</p>
<p>收获还是蛮多的,主要在于wenpack使用的一些细节问题,有点茅塞顿悟的体验吧,另外在node上也不再一脸懵逼了。不过说实话,以现在的水平向直接使用node做点什么还是挺难的,今天测试了下链接<code>mongodb</code>和<code>mysql</code>数据库,虽然能使用,但还是怪怪的。所以就想先使用现有的框架,再反推学习node。</p>
<p>框架的话就选了这个<code>express</code>.</p>
<p>主要就是测试了几个书里提到的中间件,书写的有些早,很多api都过时了,照着官网一点一点找更新的地方看。</p>
<p>目前觉得对我有用的是:<code>multer</code>和<code>static</code>。</p>
<p>后者可以在本地调试页面,对于手机页面尤其有用。</p>
<p>这次主要说一下<code>multer</code>,我并没有实现所有的功能,只是实现单图片上传这一个功能,其他的再摸索喽。</p>
<p><img src="/img/remote/1460000011740833" alt="" title=""></p>
<p>这是文件的整个目录,主要就两个,一个是根目录下的<code>main.js</code>,还有一个是<code>public/index.html</code>。</p>
<p>放代码:</p>
<pre><code class="js">//main.js
let express = require('express');
var multer = require('multer')
var storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, 'public/');
},
filename: function (req, file, cb) {
cb(null, Date.now() + '.png');
}
})
var upload = multer({ storage: storage })
//var upload = multer({ dest: 'public/' })
let app = express()
app.use(express.static('public'))
app.post('/public/index.html',upload.single('myfile'),(req,res,next)=>{
console.log(req.file)
res.send(req.file)
})
app.listen(3300,'127.0.0.1')</code></pre>
<pre><code class="html"><!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<input type="file" id="file" accept="image/*">
<div id="result"></div>
<img src="" alt="" id="img" width="40" height="40">
<script>
let file = document.getElementById('file');
file.onchange = function (e) {
let file = e.target.files[0];
let xhr = new XMLHttpRequest();
let fd = new FormData();
fd.append('myfile', file)
xhr.open('post', '/public/index.html')
xhr.onload = function () {
// console.log(xhr)
if (xhr.status === 200) {
let data = JSON.parse(xhr.responseText)
document.getElementById('result').innerHTML = this.response
document.getElementById('img').src = data.filename
}
}
xhr.send(fd)
}
</script>
</body>
</html></code></pre>
<p>不想引用jquery库,我就原生写的ajax,总的来说应该没什么难的,总之就是点击按钮选择完图片之后,会将图片的信息放在一个键名为<code>myfile</code>的对象中,传给后台。</p>
<p>express把接受到的图片存储在<code>/public/</code>文件下,这里有个小小的坑。可以看到我在<code>main.js</code>注释了这样一行代码:</p>
<pre><code>var upload = multer({ dest: 'public/' })
</code></pre>
<p>其实最开始的时候我用的就是这一行代码,<code>dest</code>的意思是选择一个路径去存储文件,但是这样写有一个小小的问题,存入进来的文件是没有后缀名的。</p>
<p>我在向前台返回数据的时候</p>
<pre><code>res.send(req.file)
</code></pre>
<p>这个问题就很严重,比如一个场景是我上传一张图片做头像,但是等我下次进入自己的个人页面,后台给我返回的数据没有办法作为图片的地址使用,这就很麻烦了。所以在网上找了一个原因,就把上面的代码注释换成了这个:</p>
<pre><code>var storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, 'public/');
},
filename: function (req, file, cb) {
cb(null, Date.now() + '.png');
}
})
var upload = multer({ storage: storage })
</code></pre>
<p><code>destination</code>是文件存储的地址,<code>filename</code>设置的是文件的名字,那在这里如果写成这种:</p>
<pre><code>filename: function (req, file, cb) {
cb(null, file.fieldname + '.png');
}
</code></pre>
<p>你会发现你传入的每一张图片的名字都是<code>myfile.png</code>,新的覆盖旧的。所以为了能保存传入的所有图片,我就使用Date.now()作为每张图片不同的识别符,这样就不会再出现覆盖的情况。</p>
<p>目前就这样,下次弄出来了多图片上传我再接着更新。</p>
知乎粒子束的实现
https://segmentfault.com/a/1190000011480393
2017-10-09T18:58:08+08:00
2017-10-09T18:58:08+08:00
Aria
https://segmentfault.com/u/janesu
0
<p>最近上手了canvas,正好看见一个知乎粒子束的实现,觉得蛮有意思的,自己就照着做了一遍。原效果是用es6实现的,我这篇文章也就用es6的语法讲了,但是可能有些人对es6的语法不熟悉,我又用es5的语法写了一遍,一方面加深理解,一方面也可以练习一下es5继承的实现,这些都放在仓库里了,可以根据需要自己查看。</p>
<blockquote><p><a href="https://link.segmentfault.com/?enc=oTZ6FbQCpUyZ1zHE0upE%2Bg%3D%3D.%2FhxIUYgiXGjYVBfJTerAybAYBdN%2BVbOC7yJAaT5My8HyYiMR0Co3DOdIwWhNskjg" rel="nofollow">仓库地址</a><br><a href="https://link.segmentfault.com/?enc=u21EonPgOQ%2F%2BzUhK8V14wQ%3D%3D.HSpnHYfd9YegCWE7PakbHMnOyT0UrE%2F9KwMMMgtyy0SR5eTZi1M7toIWIJnm2Xtn" rel="nofollow">效果地址</a></p></blockquote>
<h2>整体框架</h2>
<p>这个效果大体可以分为两个部分:</p>
<ol>
<li>进入页面初始化粒子束</li>
<li>当鼠标进入页面,在当前坐标画一个圆,并和初始化的效果进行交互。</li>
</ol>
<p>具体效果是:</p>
<ol>
<li>在页面随机位置画圆</li>
<li>圆以一定的速度在页面移动</li>
<li>当两个圆靠近时,链接一条线</li>
</ol>
<p>分析完需求之后,无论是初始化还是鼠标的交互,都离不开下面那三种具体的效果。唯一不同的地方在于,当鼠标进入页面的时候,圆圈产生的位置不是固定的,而是以鼠标的坐标为准,因此这个方法对于鼠标的行为来说是独立的。因此,最开始的结构就可以这样写:</p>
<pre><code class="js">class Circle{
// 父类
// Circle的构造函数
constructor() {}
//以下是circle原型上的方法
//方法1 画圆
drawCircle(){}
//方法2 移动
move(){}
//方法3 连线
drawLine(){}
}
class currentCircle extends Circle{
// 鼠标的对象,也就是子类
// 继承父类的构造函数的属性
constructor(x, y) {}
// 新增一个自己的方法
// 当鼠标进入页面,在鼠标坐标画圆
drawCircle(){}
}</code></pre>
<h2>具体实现</h2>
<p>就这样,基本的结构就完成了,我们来具体看一下这个结构,在<code>Circle</code>(之后统称为父类),定义了一个构造函数,这里面都是canvas画图用到的相关属性,按照我们的需求,这里面需要有圆的x坐标,y坐标,圆的半径,圆每次移动的距离,那就可以这样写:</p>
<pre><code class="js">// 父类
constructor(x, y) {
this.x = x;
this.y = y;
this.r = Math.random() * 10; //圆的半径
this._mx = Math.random(); //圆在x轴上移动的距离
this._my = Math.random(); //圆在y轴上移动的距离
}</code></pre>
<p>这里面,之所以只有x,y需要以参数的形式定义,先猜猜为什么?</p>
<p>前面提到过,无论是初始化效果还是鼠标的交互,只有一个地方不一样,就是后者的鼠标坐标就是新产生的圆的坐标,而非随机的。<code>currentCircle</code>(之后统称为子类)继承了父类构造函数中的属性,所以只有以参数的形式传入才能灵活的选择是随机还是鼠标坐标定义圆的位置。如果现在不好理解的话,等文章结束,就会明白了。</p>
<p>完成属性之后,我们就来完善父类的方法。</p>
<p>无论是画圆还是说连线,都需要用到canvas,因此方法内部都要用到canvas的2D上下文对象,这个既可以用参数传入。</p>
<p>连线的方法,不仅要知道线的起始点在哪,还需要知道重点在哪,起始点很好确定,当前圆的中心点的坐标即可,终点则不好确定,因此我们可以把另一个圆作为参数传入,读取它的坐标,因此就是这样:</p>
<pre><code class="js">//父类
drawCircle(ctx) {
ctx.beginPath();
ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2, false);
ctx.closePath();
ctx.fillStyle = 'rgba(204, 204, 204, 0.3)';
ctx.fill();
}
drawLine(ctx, _circle) {
// _circle就是需要产生连线的另一个圆
let dx = this.x - _circle.x; // 两个圆心在x轴上的距离
let dy = this.y - _circle.y; // 两个圆心在y轴上的距离
let d = Math.sqrt(dx * dx + dy * dy) // 利用三角函数计算出两个圆心之间的距离
if (d < 150) {
ctx.beginPath();
ctx.moveTo(this.x, this.y); // 线的起点
ctx.lineTo(_circle.x, _circle.y); // 线的终点
ctx.closePath();
ctx.strokeStyle = 'rgba(204, 204, 204, 0.3)';
ctx.stroke();
}
}</code></pre>
<p>之前我也说过,线的产生是在两个圆接近的地方产生,否则就不画线,因此需要判断距离,代码中的距离是150像素,这个根据需求可以随意改。</p>
<p>最后就是移动啦:-D</p>
<p>那首先,我们是不是得保证所有效果的实现都是在canvas里面,不允许有超出的现象发生,如果碰到边界了,应该返回去。氮素每个人的电脑屏幕又不一样大,因此这个大小就不能是固定的,因此就只能写成参数的形式了。</p>
<pre><code class="js">//父类
move(w, h) {
this._mx = (this.x < w && this.x > 0) ? this._mx : (-this._mx);
this._my = (this.y < h && this.y > 0) ? this._my : (-this._my);
this.x += this._mx / 2; // (this._mx / 2)越大,移动越快,下同
this.y += this._my / 2;
}</code></pre>
<p>这里面,w和h分别代表画布的宽和高,我具体想说一下里面对距离的判断。</p>
<p>根据写法可以看出来,会先判断这个圆的x坐标和y坐标是不是在画布内。<br>如果是,就给一个正值。<br>如果不是,就给一个负值。</p>
<p>但我也在担心,如果圆一开始就向左边或者上面移动,那不就移动的距离变负值,飘出页面了么?不知道有没有人看出来我这个想法有多蠢。</p>
<p>首先,无论是初始化的效果,亦或是鼠标交互产生的圆,能确定的是他们一定在画布的范围内。所以一开始对于移动距离的判断就肯定是正值,这样的话,圆的移动方向就是向右或者向下这个范围里的一个方向所以他们的结果就是一定会先碰到右边和下边的边界,此时,距离为负值,向相反的方向移动,下次再碰到左边和上边的边界时,距离为正值,在向相反的方向运动,不断循环。因此效果根本不会跑出圈外。</p>
<p>至此,父类的内容就写完了,相比,子类其实就很简单了,一个是继承属性,一个是修改方法。</p>
<pre><code class="js">// 子类
constructor(x, y) {
super(x, y)
}
drwaCircle(ctx) {
ctx.beginPath();
this.r = 8
ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2, false)
ctx.fillStyle = 'rgba(255, 77, 54, 0.6)'
ctx.fill();
}</code></pre>
<p>子类的<code>drwaCircle</code>方法和父类的<code>drwaCircle</code>方法不同的地方在于,前者的圆半径是固定的,如果说你希望半径随机,这个方法就不必改写,直接继承父类的就可以。</p>
<p>父类和子类的问题解决之后,我们来看一些公共的属性和方法。</p>
<pre><code class="js">let canvas = document.createElement('canvas')
document.body.appendChild(canvas)
let ctx = canvas.getContext('2d');
let w = canvas.width = canvas.offsetWidth;
let h = canvas.height = canvas.offsetHeight;
let circles = [];
let current_circle = new currentCircle(0, 0)</code></pre>
<p>这里面我主要说一下这两句</p>
<pre><code>let circles = [];
let current_circle = new currentCircle(0, 0)
</code></pre>
<p><code>circles</code>从定义看就是一个空数组,那么它的意义是什么呢?</p>
<p>我们最初的目的就是在画布中画一个个的圆,并且这些圆都按照自己的方向移动,靠近还会连线,那这每一个圆就可以看做是一个对象,每一个对象都包含这个圆的x坐标,y左边,半径,移动的距离这些基本信息,然后基于这些信息画圆,移动,再和另一个圆交互划线。</p>
<p>因此这个<code>circles</code>就是储存了页面中所有圆圈对象的一个集合。那肯定我们得先创建这么一个集合:</p>
<pre><code class="js">let init = (num)=>{
for(let i =0;i<num;i++){
circles.push(new Circle(Math.random()*w,Math.random()*h))
}
}</code></pre>
<p><code>num</code>就是页面中圆的个数,也是<code>circle</code>的length。至于循环,就是按照你需要的个数创建父类的实例,每一个实例都有自己的各种属性,然后将他们添加到集合中。这样就完成了对数组的初始化。</p>
<p>再看后面那句。</p>
<p>这里创建了一个子类的实例,这个实例是用来进行鼠标交互的,这里创建实例的时候,传入的x和y都是0,这个很重要,后面再说为什么。</p>
<p>现在,我们初始化了所有的圆,实例化了鼠标的行为,创建好了画布,但只是这样,浏览器是不知道我们要干什么的,我们现在还需要一个方法告诉浏览器我们要做什么。</p>
<p>关于这个方法,我们得告诉浏览器,你需要按照我给定的数目画圆,每个圆按照一定的频率和距离移动,然后两个圆还得连线。现在数组已经有了,就这样写:</p>
<pre><code class="js">let draw = ()=>{
for(let i=0;i<circle.length;i++){
// 这里遍历了数组的每一个对象
// 那这个对象先要用方法把自己按照自己的属性画出来
// 再按照属性规定的方式移动
circle[i].drwaCircle(ctx)
circle[i].move(w,h)
for(let j =i+1;j<circle.length;j++){
// 之前说过,划线需要有一个起始点和一个终止点
// 起始点很好解决,就是调用该方法的圆的坐标
// 终止点就可以遍历数组中的其他对象,如果这个对象的距离小于我们规定的距离,划线成功,反之就不画线
circle[i].drawLine(circle[j])
}
}
}</code></pre>
<p>但是这样够么?我们这里只是告诉了浏览器一开始怎么做,但是没有告诉浏览器鼠标进入该怎么办。但是我们得先判断鼠标有没有进入页面,也就是有没有x值和y值产生。</p>
<p>记得之前在初始化鼠标实例的时候传入了两个0么,正好就可以借助这个判断一下:</p>
<pre><code class="js">let draw = ()=>{
for(let i=0;i<circle.length;i++){
// 这里遍历了数组的每一个对象
// 那这个对象先要用方法把自己按照自己的属性画出来
// 再按照属性规定的方式移动
circle[i].drwaCircle(ctx)
circle[i].move(w,h)
for(let j =i+1;j<circle.length;j++){
// 之前说过,划线需要有一个起始点和一个终止点
// 起始点很好解决,就是调用该方法的圆的坐标
// 终止点就可以遍历数组中的其他对象,如果这个对象的距离小于我们规定的距离,划线成功,反之就不画线
circle[i].drawLine(ctx,circle[j])
}
}
if(current_circle.x){
current_circle.drawCircle(ctx)
for(let i=0;i<circle.length;i++){
current_circle.drawLine(ctx,circle[i])
}
}
}</code></pre>
<p>这样告诉浏览器该干什么就完成了,但是这个方法只会执行一遍,而我们需要的是动画效果,所以还需要一个计时器,这里推荐使用新的API:<code>requestAnimationFrame</code>。</p>
<p>这个方法非常适用于动画效果,我们知道,计时器并不是那么完美,至少,他不一定会按照你给的时间间隔运行,而这个方法是按照屏幕的刷新频率运行的,因此动画效果更流畅。</p>
<p>酱紫,这个方法就写完了:</p>
<pre><code class="js">let draw = ()=>{
for(let i=0;i<circle.length;i++){
// 这里遍历了数组的每一个对象
// 那这个对象先要用方法把自己按照自己的属性画出来
// 再按照属性规定的方式移动
circle[i].drwaCircle(ctx)
circle[i].move(w,h)
for(let j =i+1;j<circle.length;j++){
// 之前说过,划线需要有一个起始点和一个终止点
// 起始点很好解决,就是调用该方法的圆的坐标
// 终止点就可以遍历数组中的其他对象,如果这个对象的距离小于我们规定的距离,划线成功,反之就不画线
circle[i].drawLine(ctx,circle[j])
}
}
if(current_circle.x){
current_circle.drawCircle(ctx)
for(let i=0;i<circle.length;i++){
current_circle.drawLine(ctx,circle[i])
}
}
requestAnimationFrame(draw)
}</code></pre>
<p>然后把这个方法写进初始化的方法里:</p>
<pre><code class="js">let init = (num)=>{
for(let i =0;i<num;i++){
circles.push(new Circle(Math.random()*w,Math.random()*h))
}
}
draw();</code></pre>
<p>之后再告诉浏览器什么时候进行初始化:</p>
<pre><code class="js">window.addEventListener('load', init(200));
window.onmousemove = function (e) {
e = e || window.event;
current_circle.x = e.clientX;
current_circle.y = e.clientY;
}
window.onmouseout = function () {
current_circle.x = null;
current_circle.y = null;
};</code></pre>
<p>然后监控鼠标何时进入页面,监测其坐标并把值附给鼠标实例。</p>
<p>酱紫,整个效果就完成了,因为代码是用es6语法写的,因此需要了解一些该语法的特性,如果实在看不明白,可以对照着es5版本的语法一起看。</p>
<p>谢谢大家。</p>
由插件封装引出的一丢丢思考
https://segmentfault.com/a/1190000011302580
2017-09-21T23:32:11+08:00
2017-09-21T23:32:11+08:00
Aria
https://segmentfault.com/u/janesu
3
<p>今天看一个妹子写的canvas的插件,好羞愧啊,比我小还比我厉害得多,氮素,得向厉害的的人学习呀。所以就拜读了源码,业务方面的东西我就不说了,我也没仔细看,主要是被下面这一部分代码吸引了。</p>
<pre><code class="javascript"> _global = (function() {
return this || (0, eval)('this');
}());
if (typeof module !== "undefined" && module.exports) {
module.exports = CanvasStar;
} else if (typeof define === "function" && define.amd) {
define(function() {
return CanvasStar;
});
} else {
!('CanvasStar' in _global) && (_global.CanvasStar = CanvasStar);
}</code></pre>
<p>细细琢磨了一会,看懂了<code>if</code>和<code>else if</code>判断的用意。</p>
<p>在这之前先说明下<code>CanvasStar</code>是什么。代码里有这样一句。</p>
<pre><code class="js">function CanvasStar() {}</code></pre>
<p>所以这个方法就是在代码里执行这个canvas的入口,其他所有相关的内容都作为一个对象赋值给了他的原型对象。</p>
<p>再说回那两个判断,因为在es6之前,都用的是<code>commonJS</code>和<code>AMD</code>规范进行代码加载,所以含义就在于当前的环境支不支持<code>commonjs</code>或者<code>AMD</code>规范。在<code>HTML</code>文件里引用的话,这两个就先跳过吧。主要看这两句。</p>
<pre><code class="js">//问题1
_global = (function() {
return this || (0, eval)('this');
}());
//问题2
else{
!('CanvasStar' in _global) && (_global.CanvasStar = CanvasStar);
}</code></pre>
<p>我google了<code>(0, eval)('this')</code>,有篇文章是这么说的:</p>
<blockquote><p>无论如何方式调用<code>(0, eval)('this')</code>,返回的都是全局对象</p></blockquote>
<p>所以<strong>问题1</strong>其实就是在将全局环境(也就是window)赋值给一个变量。我<code>console</code>了<code>this</code>,按理说,这里的<code>this</code>指向的就应该是全局变量,为什么还要后面的代码重新指向全局呢?</p>
<p>然后打算重新看一遍代码的时候发现她在写这个插件的时候用的是严格模式,所以这里的<code>this</code>只可能是<code>underfined</code>。我贴一下MDN对于严格模式下<code>this</code>的指向。</p>
<blockquote><p>在严格模式下通过this传递给一个函数的值不会被强制转换为一个对象。对一个普通的函数来说,this总会是一个对象:不管调用时this它本来就是一个对象;还是用布尔值,字符串或者数字调用函数时函数里面被封装成对象的this;还是使用undefined或者null调用函数式this代表的全局对象(使用call, apply或者bind方法来指定一个确定的this)。这种自动转化为对象的过程不仅是一种性能上的损耗,同时在浏览器中暴露出全局对象也会成为安全隐患,因为全局对象提供了访问那些所谓安全的JavaScript环境必须限制的功能的途径。所以对于一个开启严格模式的函数,指定的this不再被封装为对象,而且如果没有指定this的话它值是undefined</p></blockquote>
<p>很长是吧,简短的说,在严格模式下,如果没有给<code>this</code>指定值的话,它就是未定义的。所以在赋值的时候就跳过了这个<code>this</code>,返回了<code>(0, eval)('this')</code>。</p>
<p>这里说明一下<code>eval</code>,在我找资料的过程中,都提到它的两种使用方式<strong>间接eval调用和直接eval调用</strong>,这两种的调用方式的结果完全不同,一般我见到的都是<code>直接eval调用</code>,甚至于由于不提倡使用,所以<code>eval</code>几乎很少出现。</p>
<p>等我在看多一点资料以后在写一个<code>eval</code>相关的博文吧。但我可以先对这里面的<strong>逗号操作符</strong>做一点说明。</p>
<h2>逗号操作符</h2>
<p>这是MDN上的解释</p>
<blockquote><p>逗号操作符 对它的每个操作数求值(从左到右),并返回最后一个操作数的值。</p></blockquote>
<p>我就用几个代码说明一下</p>
<pre><code class="js">function func1() {
let a = '我是第一个赋值方法'
console.log('一号喵')
return a
}
function func2() {
let b = '我是第二个赋值方法'
console.log('二号喵')
return b
}
let c = (func1(), func2())
console.log(c)</code></pre>
<p>猜猜这里有几个<code>console</code>,分别是什么。</p>
<p>现在揭晓答案</p>
<pre><code class="js">//console.log结果
一号喵
二号喵
我是第二个赋值方法</code></pre>
<p>所以根据定义来看,在对<code>c</code>赋值的过程中,从左至右依次执行了<code>func1</code>和<code>func2</code>两个方法,但是在赋值的时候,只返回了最后的那个值,也就是<code>func2</code>里写的<code>return</code>。</p>
<p>所以我们在看一下<code>eval</code></p>
<pre><code>(0, eval)
</code></pre>
<p>这里返回的也是<code>eval</code>,等同于这个</p>
<pre><code>eval('this')
</code></pre>
<p>然而还是因为调用方式的不一样,所以最后的结果不一样,先按下不表了。</p>
<h2>立即执行函数的公与私</h2>
<p>那再来看问题2就简单明了多了,他就是在判断全局是否存在<code>CanvasStar</code>这个方法,如果不存在,就在全局创建一个变量并将内部的方法赋值给他。</p>
<p>但这里就涉及一个问题,像是我,单独写js文件并引入使用的时候,都是直接调取方法使用,为什么这么麻烦啊,所以这里我也尝试在HTML文件里直接调用<code>CanvasStar</code>(前提是把那些代码注释了)。</p>
<p>但很可惜,浏览器报错:</p>
<pre><code>Uncaught TypeError: CanvasStar is not a constructor
</code></pre>
<p>所以这里我就想说说共有方法和私有方法,代码如下</p>
<pre><code class="js">//main.js
(function() {
let a = '猜猜我是什么类型'
function sum() {
console.log(a)
}
let log = function() {
console.log(a)
}
})()</code></pre>
<p>然后html文件里调用:</p>
<pre><code class="js">sum(); // Uncaught ReferenceError: sum is not defined
log(); // Uncaught ReferenceError: log is not defined</code></pre>
<p>我对<code>main.js</code>的文件做一丢丢修改</p>
<pre><code class="js">//main.js
(function() {
let a = '猜猜我是什么类型'
log = function() {
console.log(a)
}
function sum() {
console.log(a)
}
})()</code></pre>
<p>重新运行:</p>
<pre><code class="js">
log(); // 猜猜我是什么类型
sum(); // Uncaught ReferenceError: sum is not defined
</code></pre>
<p>我第一次在js文件里写了一个函数声明和一个函数表达式,但是在外部都无法调用,第二次我把函数表达式赋值的变量声明去掉之后,就能正常访问了。</p>
<p>这个问题的关键在作用域,当我建立这个立即执行函数是,作用域链是这样的:</p>
<table>
<thead><tr><th align="center">全局作用域</th></tr></thead>
<tbody><tr><td align="center">匿名函数</td></tr></tbody>
</table>
<table>
<thead><tr><th align="center">函数作用域</th></tr></thead>
<tbody>
<tr><td align="center">变量a</td></tr>
<tr><td align="center">log函数</td></tr>
<tr><td align="center">sun函数</td></tr>
</tbody>
</table>
<p>而当匿名函数执行完之后,它本身的作用域就被销毁了,从他的上一级,也就是全局作用域根本访问不到任何东西,但如果在进行函数赋值时,赋值的变量并没有经过<code>var</code>或者<code>let</code>生明,在这里<code>log</code>这个变量是被写在全局作用域里面的,所以外部直接调用完全没问题。</p>
<p>所以得出的一个结论是:讲过<code>let</code>或者<code>var</code>生明的变量都是私有的,函数声明一定是私有的方法。其他都是共有变量或者方法。另外,共有方法能访问作用域里的私有变量,但是私有变量无法从外部直接获取。</p>
<p>其实这也就是某种意义上的闭包啦。</p>
<h2>另一种封装方法</h2>
<p>要是只讲上面的多没意思啊,正好我最近在看underscore的源码,我就想着看看人家的封装方法是啥。</p>
<p>在规范判断那一块大同小异,就不说了,但是对于全局变量的赋值走的是一条完全不同的路。</p>
<pre><code class="js">(function() {
let root = this;
............
.............
root._ = _
}.call(this))</code></pre>
<p>这样外部直接</p>
<pre><code>_.方法名;
</code></pre>
<p>就可以使用了。</p>
<p>那在这里,underscore在执行这段匿名函数的时候,使用<code>call</code>将函数的<code>this</code>指向了全局变量,这里就是<code>this</code>,可能这句话比较绕,但事实就是这样。如果实在理解不了,我举个例子:</p>
<blockquote><p>一艘船在海上航行,夜间,如果天空晴朗,<strong>指的是一般模式</strong>,那水手可以根据天上的星辰判断方位,如果不幸乌云密布,就是<strong>严格模式</strong>,那就迷路啦,但恰好,转过一个海湾,发现了一座著名的灯塔,重新给你指引了方向,这就是<strong>call重新指向当前作用域的this,也就是全局</strong>。</p></blockquote>
<p>不知道我有没有说清楚呀。</p>
用vue构建多页面应用
https://segmentfault.com/a/1190000011265006
2017-09-19T21:08:34+08:00
2017-09-19T21:08:34+08:00
Aria
https://segmentfault.com/u/janesu
141
<p>最近一直在研究使用vue做出来一些东西,但都是SPA的单页面应用,但实际工作中,单页面并不一定符合业务需求,所以这篇我就来说说怎么开发多页面的Vue应用,以及在这个过程会遇到的问题。</p>
<p>这是我放在GitHub上的项目,里面有整个配置文件,可以参看一下:<a href="https://link.segmentfault.com/?enc=eo47YVBQOzoycnGeVtH67g%3D%3D.aJA1uKCT9GZ4xL6YqIa2Z3wHlVivvmfCP7eCYEa7s3DSla5w%2FrSpOGMwG75bahTH" rel="nofollow">multiple-vue-page</a></p>
<h2>准备工作</h2>
<p>在本地用<code>vue-cli</code>新建一个项目,这个步骤vue的官网上有,我就不再说了。</p>
<p>这里有一个地方需要改一下,在执行<code>npm install</code>命令之前,在<code>package.json</code>里添加一个依赖,后面会用到。</p>
<p><img src="/img/remote/1460000011265011" alt="" title=""></p>
<h2>修改webpack配置</h2>
<p>这里展示一下我的项目目录</p>
<pre><code>├── README.md
├── build
│ ├── build.js
│ ├── check-versions.js
│ ├── dev-client.js
│ ├── dev-server.js
│ ├── utils.js
│ ├── vue-loader.conf.js
│ ├── webpack.base.conf.js
│ ├── webpack.dev.conf.js
│ └── webpack.prod.conf.js
├── config
│ ├── dev.env.js
│ ├── index.js
│ └── prod.env.js
├── package.json
├── src
│ ├── assets
│ │ └── logo.png
│ ├── components
│ │ ├── Hello.vue
│ │ └── cell.vue
│ └── pages
│ ├── cell
│ │ ├── cell.html
│ │ ├── cell.js
│ │ └── cell.vue
│ └── index
│ ├── index.html
│ ├── index.js
│ ├── index.vue
│ └── router
│ └── index.js
└── static</code></pre>
<p>在这一步里我们需要改动的文件都在<code>build</code>文件下,分别是:</p>
<ul>
<li>utils.js</li>
<li>webpack.base.conf.js</li>
<li>webpack.dev.conf.js</li>
<li>webpack.prod.conf.js</li>
</ul>
<p>我就按照顺序放出完整的文件内容,然后在做修改或添加的位置用注释符标注出来:</p>
<h3>utils.js文件</h3>
<pre><code class="javascript">// utils.js文件
var path = require('path')
var config = require('../config')
var ExtractTextPlugin = require('extract-text-webpack-plugin')
exports.assetsPath = function (_path) {
var assetsSubDirectory = process.env.NODE_ENV === 'production' ?
config.build.assetsSubDirectory :
config.dev.assetsSubDirectory
return path.posix.join(assetsSubDirectory, _path)
}
exports.cssLoaders = function (options) {
options = options || {}
var cssLoader = {
loader: 'css-loader',
options: {
minimize: process.env.NODE_ENV === 'production',
sourceMap: options.sourceMap
}
}
// generate loader string to be used with extract text plugin
function generateLoaders(loader, loaderOptions) {
var loaders = [cssLoader]
if (loader) {
loaders.push({
loader: loader + '-loader',
options: Object.assign({}, loaderOptions, {
sourceMap: options.sourceMap
})
})
}
// Extract CSS when that option is specified
// (which is the case during production build)
if (options.extract) {
return ExtractTextPlugin.extract({
use: loaders,
fallback: 'vue-style-loader'
})
} else {
return ['vue-style-loader'].concat(loaders)
}
}
// https://vue-loader.vuejs.org/en/configurations/extract-css.html
return {
css: generateLoaders(),
postcss: generateLoaders(),
less: generateLoaders('less'),
sass: generateLoaders('sass', { indentedSyntax: true }),
scss: generateLoaders('sass'),
stylus: generateLoaders('stylus'),
styl: generateLoaders('stylus')
}
}
// Generate loaders for standalone style files (outside of .vue)
exports.styleLoaders = function (options) {
var output = []
var loaders = exports.cssLoaders(options)
for (var extension in loaders) {
var loader = loaders[extension]
output.push({
test: new RegExp('\\.' + extension + '$'),
use: loader
})
}
return output
}
/* 这里是添加的部分 ---------------------------- 开始 */
// glob是webpack安装时依赖的一个第三方模块,还模块允许你使用 *等符号, 例如lib/*.js就是获取lib文件夹下的所有js后缀名的文件
var glob = require('glob')
// 页面模板
var HtmlWebpackPlugin = require('html-webpack-plugin')
// 取得相应的页面路径,因为之前的配置,所以是src文件夹下的pages文件夹
var PAGE_PATH = path.resolve(__dirname, '../src/pages')
// 用于做相应的merge处理
var merge = require('webpack-merge')
//多入口配置
// 通过glob模块读取pages文件夹下的所有对应文件夹下的js后缀文件,如果该文件存在
// 那么就作为入口处理
exports.entries = function () {
var entryFiles = glob.sync(PAGE_PATH + '/*/*.js')
var map = {}
entryFiles.forEach((filePath) => {
var filename = filePath.substring(filePath.lastIndexOf('\/') + 1, filePath.lastIndexOf('.'))
map[filename] = filePath
})
return map
}
//多页面输出配置
// 与上面的多页面入口配置相同,读取pages文件夹下的对应的html后缀文件,然后放入数组中
exports.htmlPlugin = function () {
let entryHtml = glob.sync(PAGE_PATH + '/*/*.html')
let arr = []
entryHtml.forEach((filePath) => {
let filename = filePath.substring(filePath.lastIndexOf('\/') + 1, filePath.lastIndexOf('.'))
let conf = {
// 模板来源
template: filePath,
// 文件名称
filename: filename + '.html',
// 页面模板需要加对应的js脚本,如果不加这行则每个页面都会引入所有的js脚本
chunks: ['manifest', 'vendor', filename],
inject: true
}
if (process.env.NODE_ENV === 'production') {
conf = merge(conf, {
minify: {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true
},
chunksSortMode: 'dependency'
})
}
arr.push(new HtmlWebpackPlugin(conf))
})
return arr
}
/* 这里是添加的部分 ---------------------------- 结束 */</code></pre>
<h3>webpack.base.conf.js 文件</h3>
<pre><code class="javascript">// webpack.base.conf.js 文件
var path = require('path')
var utils = require('./utils')
var config = require('../config')
var vueLoaderConfig = require('./vue-loader.conf')
function resolve(dir) {
return path.join(__dirname, '..', dir)
}
module.exports = {
/* 修改部分 ---------------- 开始 */
entry: utils.entries(),
/* 修改部分 ---------------- 结束 */
output: {
path: config.build.assetsRoot,
filename: '[name].js',
publicPath: process.env.NODE_ENV === 'production' ?
config.build.assetsPublicPath :
config.dev.assetsPublicPath
},
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
'vue$': 'vue/dist/vue.esm.js',
'@': resolve('src'),
'pages': resolve('src/pages'),
'components': resolve('src/components')
}
},
module: {
rules: [{
test: /\.vue$/,
loader: 'vue-loader',
options: vueLoaderConfig
},
{
test: /\.js$/,
loader: 'babel-loader',
include: [resolve('src'), resolve('test')]
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('img/[name].[hash:7].[ext]')
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
}
}
]
}
}</code></pre>
<h3>webpack.dev.conf.js 文件</h3>
<pre><code class="javasctipt">var utils = require('./utils')
var webpack = require('webpack')
var config = require('../config')
var merge = require('webpack-merge')
var baseWebpackConfig = require('./webpack.base.conf')
var HtmlWebpackPlugin = require('html-webpack-plugin')
var FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
// add hot-reload related code to entry chunks
Object.keys(baseWebpackConfig.entry).forEach(function (name) {
baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name])
})
module.exports = merge(baseWebpackConfig, {
module: {
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap })
},
// cheap-module-eval-source-map is faster for development
devtool: '#cheap-module-eval-source-map',
plugins: [
new webpack.DefinePlugin({
'process.env': config.dev.env
}),
// https://github.com/glenjamin/webpack-hot-middleware#installation--usage
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin(),
// https://github.com/ampedandwired/html-webpack-plugin
/* 注释这个区域的文件 ------------- 开始 */
// new HtmlWebpackPlugin({
// filename: 'index.html',
// template: 'index.html',
// inject: true
// }),
/* 注释这个区域的文件 ------------- 结束 */
new FriendlyErrorsPlugin()
/* 添加 .concat(utils.htmlPlugin()) ------------------ */
].concat(utils.htmlPlugin())
})</code></pre>
<h3>webpack.prod.conf.js 文件</h3>
<pre><code class="javascript">var path = require('path')
var utils = require('./utils')
var webpack = require('webpack')
var config = require('../config')
var merge = require('webpack-merge')
var baseWebpackConfig = require('./webpack.base.conf')
var CopyWebpackPlugin = require('copy-webpack-plugin')
var HtmlWebpackPlugin = require('html-webpack-plugin')
var ExtractTextPlugin = require('extract-text-webpack-plugin')
var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
var env = config.build.env
var webpackConfig = merge(baseWebpackConfig, {
module: {
rules: utils.styleLoaders({
sourceMap: config.build.productionSourceMap,
extract: true
})
},
devtool: config.build.productionSourceMap ? '#source-map' : false,
output: {
path: config.build.assetsRoot,
filename: utils.assetsPath('js/[name].[chunkhash].js'),
chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
},
plugins: [
// http://vuejs.github.io/vue-loader/en/workflow/production.html
new webpack.DefinePlugin({
'process.env': env
}),
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
},
sourceMap: true
}),
// extract css into its own file
new ExtractTextPlugin({
filename: utils.assetsPath('css/[name].[contenthash].css')
}),
// Compress extracted CSS. We are using this plugin so that possible
// duplicated CSS from different components can be deduped.
new OptimizeCSSPlugin({
cssProcessorOptions: {
safe: true
}
}),
// generate dist index.html with correct asset hash for caching.
// you can customize output by editing /index.html
// see https://github.com/ampedandwired/html-webpack-plugin
/* 注释这个区域的内容 ---------------------- 开始 */
// new HtmlWebpackPlugin({
// filename: config.build.index,
// template: 'index.html',
// inject: true,
// minify: {
// removeComments: true,
// collapseWhitespace: true,
// removeAttributeQuotes: true
// // more options:
// // https://github.com/kangax/html-minifier#options-quick-reference
// },
// // necessary to consistently work with multiple chunks via CommonsChunkPlugin
// chunksSortMode: 'dependency'
// }),
/* 注释这个区域的内容 ---------------------- 结束 */
// split vendor js into its own file
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: function (module, count) {
// any required modules inside node_modules are extracted to vendor
return (
module.resource &&
/\.js$/.test(module.resource) &&
module.resource.indexOf(
path.join(__dirname, '../node_modules')
) === 0
)
}
}),
// extract webpack runtime and module manifest to its own file in order to
// prevent vendor hash from being updated whenever app bundle is updated
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
chunks: ['vendor']
}),
// copy custom static assets
new CopyWebpackPlugin([{
from: path.resolve(__dirname, '../static'),
to: config.build.assetsSubDirectory,
ignore: ['.*']
}])
/* 该位置添加 .concat(utils.htmlPlugin()) ------------------- */
].concat(utils.htmlPlugin())
})
if (config.build.productionGzip) {
var CompressionWebpackPlugin = require('compression-webpack-plugin')
webpackConfig.plugins.push(
new CompressionWebpackPlugin({
asset: '[path].gz[query]',
algorithm: 'gzip',
test: new RegExp(
'\\.(' +
config.build.productionGzipExtensions.join('|') +
')$'
),
threshold: 10240,
minRatio: 0.8
})
)
}
if (config.build.bundleAnalyzerReport) {
var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
webpackConfig.plugins.push(new BundleAnalyzerPlugin())
}
module.exports = webpackConfig</code></pre>
<p>至此,webpack的配置就结束了。</p>
<p>但是还没完啦,下面继续。</p>
<h2>文件结构</h2>
<pre><code>├── src
│ ├── assets
│ │ └── logo.png
│ ├── components
│ │ ├── Hello.vue
│ │ └── cell.vue
│ └── pages
│ ├── cell
│ │ ├── cell.html
│ │ ├── cell.js
│ │ └── cell.vue
│ └── index
│ ├── index.html
│ ├── index.js
│ ├── index.vue
│ └── router
│ └── index.js</code></pre>
<p><code>src</code>就是我所使用的工程文件了,<code>assets</code>,<code>components</code>,<code>pages</code>分别是静态资源文件、组件文件、页面文件。</p>
<p>前两个就不多说,主要是页面文件里,我目前是按照项目的模块分的文件夹,你也可以按照你自己的需求调整。然后在每个模块里又有三个内容:vue文件,js文件和html文件。这三个文件的作用就相当于做spa单页面应用时,根目录的<code>index.html</code>页面模板,src文件下的<code>main.js</code>和<code>app.vue</code>的功能。</p>
<p>原先,入口文件只有一个main.js,但现在由于是多页面,因此入口页面多了,我目前就是两个:index和cell,之后如果打包,就会在<code>dist</code>文件下生成两个HTML文件:<code>index.html</code>和<code>cell.html</code>(可以参考一下单页面应用时,打包只会生成一个index.html,区别在这里)。</p>
<p>cell文件下的三个文件,就是一般模式的配置,参考index的就可以,但并不完全相同。</p>
<h2>特别注意的地方</h2>
<h3>cell.js</h3>
<p>在这个文件里,按照写法,应该是这样的吧:</p>
<pre><code class="javascript">import Vue from 'Vue'
import cell from './cell.vue'
new Vue({
el:'#app',// 这里参考cell.html和cell.vue的根节点id,保持三者一致
teleplate:'<cell/>',
components:{ cell }
})</code></pre>
<p>这个配置在运行时(npm run dev)会报错</p>
<pre><code>[Vue warn]: You are using the runtime-only build of Vue where the template compiler is not available. Either pre-compile the templates into render functions, or use the compiler-included build.
(found in <Root>)</code></pre>
<p>网上的解释是这样的:</p>
<blockquote><p>运行时构建不包含模板编译器,因此不支持 template 选项,只能用 render 选项,但即使使用运行时构建,在单文件组件中也依然可以写模板,因为单文件组件的模板会在构建时预编译为 render 函数。运行时构建比独立构建要轻量30%,只有 17.14 Kb min+gzip大小。</p></blockquote>
<p>上面一段是官方api中的解释。就是说,如果我们想使用template,我们不能直接在客户端使用npm install之后的vue。</p>
<p>也给出了相应的修改方案:</p>
<pre><code>resolve: { alias: { 'vue': 'vue/dist/vue.js' } }</code></pre>
<p>这里是修改<code>package.json</code>的resolve下的vue的配置,很多人反应这样修改之后就好了,但是我按照这个方法修改之后依然报错。然后我就想到上面提到的<code>render</code>函数,因此我的修改是针对<code>cell.js</code>文件的。</p>
<pre><code class="javascript">import Vue from 'Vue'
import cell from './cell.vue'
/* eslint-disable no-new */
new Vue({
el: '#app',
render: h => h(cell)
})
</code></pre>
<p>这里面我用<code>render</code>函数取代了组件的写法,在运行就没问题了。</p>
<h3>页面跳转</h3>
<p>既然是多页面,肯定涉及页面之间的互相跳转,就按照我这个项目举例,从index.html文件点击a标签跳转到cell.html。</p>
<p>我最开始写的是:</p>
<pre><code class="html"> <!-- index.html -->
<a href='../cell/cell.html'></a></code></pre>
<p>但这样写,不论是在开发环境还是最后测试,都会报404,找不到这个页面。</p>
<p>改成这样既可:</p>
<pre><code class="html"> <!-- index.html -->
<a href='cell.html'></a></code></pre>
<p>这样他就会自己找<code>cell.html</code>这个文件。</p>
<h3>打包后的资源路径</h3>
<p>执行<code>npm run build</code>之后,打开相应的html文件你是看不到任何东西的,查看原因是找不到相应的js文件和css文件。</p>
<p>这时候的文件结构是这样的:</p>
<pre><code>├── dist
│ ├── js
│ ├── css
│ ├── index.html
│ └── cell.html</code></pre>
<p>查看index.html文件之后会发现资源的引用路径是:</p>
<pre><code>/dist/js.........
</code></pre>
<p>这样,如果你的dist文件不是在根目录下的,就根本找不到资源。</p>
<p>方法当然也有啦,如果你不嫌麻烦,就一个文件一个文件的修改路径咯,或者像我一样偷懒,修改<code>config</code>下的<code>index.js</code>文件。具体的做法是:</p>
<pre><code class="js">build: {
env: require('./prod.env'),
index: path.resolve(__dirname, '../dist/index.html'),
assetsRoot: path.resolve(__dirname, '../dist'),
assetsSubDirectory: 'static',
assetsPublicPath: '/',
productionSourceMap: true,
// Gzip off by default as many popular static hosts such as
// Surge or Netlify already gzip all static assets for you.
// Before setting to `true`, make sure to:
// npm install --save-dev compression-webpack-plugin
productionGzip: false,
productionGzipExtensions: ['js', 'css'],
// Run the build command with an extra argument to
// View the bundle analyzer report after build finishes:
// `npm run build --report`
// Set to `true` or `false` to always turn it on or off
bundleAnalyzerReport: process.env.npm_config_report
},
</code></pre>
<p>将这里面的</p>
<pre><code>assetsPublicPath: '/',
</code></pre>
<p>改成</p>
<pre><code>assetsPublicPath: './',
</code></pre>
<p>酱紫,配置文件资源的时候找到的就是相对路径下的资源了,在重新<code>npm run build</code>看看吧。</p>