SegmentFault zhouatie的前端小站最新的文章
2020-05-06T07:00:00+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
vue-router源码实现
https://segmentfault.com/a/1190000022547798
2020-05-06T07:00:00+08:00
2020-05-06T07:00:00+08:00
我是南方大汉
https://segmentfault.com/u/woshinanfangdahan
2
<h2>vue-router源码实现</h2>
<h3>前言</h3>
<p>这个月会实现一下<code>Vue</code>, <code>Vuex</code>, <code>vue-router</code>。我会以<strong>倒推</strong>的模式边开发边写文章。话不多说开始跟着我一起撸。<a href="https://link.segmentfault.com/?enc=B7wGlON%2BbCT4LlFRu%2FB%2Bcg%3D%3D.xjbSyVn8lU3unsaZ1p1kfBqbxPlwzMOUWr9uyG7jdcv80jyCW6FccbdmO7XFeLTJ3qib7i%2F50i1uLlh3j%2BzSrDSUQkoFWpSUt67hF6TP%2B6U%3D" rel="nofollow">仓库地址</a></p>
<blockquote>本文只是实现了一个基础版本的<code>vue-router</code>.本文所写的代码,不会每个地方都做异常判断。实现一个能够体现<code>vue-router</code>核心逻辑即可。</blockquote>
<p>我大致捋了下<code>vue-router</code>的流程图如下:<br><img src="/img/remote/1460000022547801" alt="vue-router思维导图" title="vue-router思维导图"></p>
<p>在写源码之前,我先展示下<code>routes</code>的数据结构,在根据这个数据结构来进行<code>vue-router</code>的开发</p>
<pre><code class="js">const routes = [
{
path: '/',
name: 'Home',
component: Home,
},
{
path: '/about',
name: 'About',
component: () =>
import(/* webpackChunkName: "about" */ '../views/About.vue'),
children: [
{
path: 'a',
name: 'about.a',
component: {
render: (h) => <div>this is a</div>,
},
},
{
path: 'b',
name: 'about.b',
component: {
render: (h) => <div>this is b</div>,
},
},
],
},
];</code></pre>
<h3>安装 install</h3>
<p>回忆下我们平时代码里用到<code>vue-router</code>必定会有的两行代码</p>
<pre><code class="js">import VueRouter from 'vue-router';
Vue.use(VueRouter);</code></pre>
<p><code>Vue</code>在<code>use</code> <code>VueRouter</code>的时候会调用<code>VueRouter</code>的<code>install</code>方法,并传入<code>Vue</code>构造函数。</p>
<p>所以我们接下来第一步就是创建一个<code>VueRouter</code>构造函数,并实现一个<code>install</code>方法。</p>
<pre><code class="js">// vue-router/index.js
const install = (Vue) => {
Vue.mixin({
beforeCreate() {
if (this.$options.router) {
this._routerRoot = this;
this._router = this.$options.router;
} else {
this._routerRoot = this.$parent && this.$parent._routerRoot;
}
},
});
// FIXME:
Object.defineProperty(Vue.prototype, '$route', {
get() {
return 'this is $route';
},
});
Object.defineProperty(Vue.prototype, '$router', {
get() {
return this._routerRoot._router;
},
});
// FIXME:
Vue.component('router-view', {
render() {
return <div>this is router-view</div>;
},
});
// FIXME:
Vue.component('router-link', {
render() {
return <div>this is router-link</div>;
},
});
};
export default install;</code></pre>
<p><code>vue-router</code>也会与<code>vuex</code>一样,通过<code>Vue.mixin</code>方法添加<code>beforeCreate</code>生命周期,通过在这个生命周期函数,向每个组件实例挂载<code>_routerRoot</code>根实例对象,根实例会多挂载一个<code>_router</code>对象,这样子组件就可以通过<code>_routerRoot._router</code>来获取路由实例对象了。同时会往<code>Vue.prototype</code>上挂载<code>$router</code> <code>$route</code>,方便每个子组件获取。</p>
<p>并挂载全局组件<code>router-view</code>、<code>router-link</code>组件;</p>
<h3>实例</h3>
<p>写完<code>install</code>方法后,我们接下来开始动手<code>VueRouter</code>。初始化时,该在<code>constructor</code>中写些什么呢?初始化当然是为了后续的一些方法做准备了。他将包含以下几点</p>
<ul>
<li>
<p>创建<code>matcher</code>对象</p>
<ul>
<li>
<code>addRoute</code>: 用于动态添加路由</li>
<li>
<code>match</code>: 重中之重,用于根据<code>path</code>匹配出对应的<code>route</code>对象,用于<code>router-view</code>的渲染</li>
</ul>
</li>
<li>根据对应的<code>mode</code>创建对应的<code>history</code>对象(我这里开发的<code>router</code>默认使用<code>HashHistory</code>
</li>
</ul>
<pre><code class="js">class VueRouter {
constructor(options) {
this.matcher = createMatcher(options.routes);
this.history = new HashHistory();
}
init() {}
}
</code></pre>
<h4>createMatcher</h4>
<p>那么<code>createMatcher</code>到底该怎么实现?还是反推的模式,它肯定返回上面列的两个方法<code>addRoute</code> <code>match</code>方法。那么就先实现下大致的样子吧。</p>
<pre><code class="js">// create-matcher.js
function createMatcher(routes) {
function addRoute() {}
function match() {}
return {
addRoute,
match,
};
}
export default createMatcher;</code></pre>
<h5>match</h5>
<p>接下来就是实现<code>match</code>方法了。前面说过<code>match</code>是用来匹配<code>path</code>所对应的<code>route</code>对象。所以他肯定有个<code>path</code>参数,然后通过一个路由映射表来筛选出对应的<code>route</code>对象。那就开始着手编码吧!</p>
<pre><code class="js">// create-matcher.js
function match(location) {
return pathMap[location]
}</code></pre>
<p>这个时候你肯定会有个疑惑,这个<code>pathMap</code>哪里来的呢?上面我已经说过<code>pathMap</code>是一个路由映射表。那就开始着手编码吧!</p>
<pre><code class="js">// create-matcher.js
function createMatcher(routes) {
const { pathList, pathMap } = createRouteMap(routes);
function addRoute() {}
function match(locaiton) {
return pathMap[locaiton];
}
return {
addRoute,
match,
};
}</code></pre>
<p><code>match</code>方法中并不会那么简单的返回<code>route</code>对象就ok了。肯定是经过一个函数保证过返回<code>matched</code> <code>path</code>等字段。这里先忽略,等讲到<code>history</code>的时候再讲解。</p>
<h6>createRouteMap</h6>
<p>你可以看到代码中调用了<code>createRouteMap</code>,那么这个方法该怎么实现呢?首先我们已知这个方法是用来创建<code>pathMap</code>的,那么先搭一个大致的函数框架</p>
<pre><code class="js">// create-route-map.js
function createRouteMap(routes) {
const pathList = [];
const pathMap = Object.create(null);
return {
pathList,
pathMap,
};
}
export default createRouteMap;</code></pre>
<p>大致会是这个样子。执行返回路由映射表。那么该怎么创建<code>pathList</code> <code>pathMap</code>对象呢?当然用传入的<code>routes</code>来构造了。那就开始着手编码吧!</p>
<pre><code class="js">// create-route-map.js
function createRouteMap(routes) {
const pathList = [];
const pathMap = Object.create(null);
routes.forEach(route => {
addRouteRecord(route, pathList, pathMap)
})
return {
pathList,
pathMap,
};
}
function addRouteRecord (route, pathList, pathMap) {
const path = route.path
const record = {
path,
component: route.component
}
if (!pathList.includes(path)) {
pathList.push(path)
pathMap[path] = record
}
}
export default createRouteMap;</code></pre>
<p>上面我通过遍历<code>routes</code>,调用<code>addRouteRecord</code>方法,并传入<code>route</code>。<code>addRouteRecord</code>根据传入的<code>route</code>构建<code>pathList</code> <code>pathMap</code>,我想大家看代码应该就理解这段代码是什么意思了。当然上面只是非常非常基础版本的。还有一些场景没考虑到,比如传入的<code>route</code>对象还有<code>children</code>,就需要继续遍历他的<code>children</code>并调用<code>addRouteRecord</code>方法。那就开始着手编码吧!</p>
<pre><code class="js">// create-route-map.js
function addRouteRecord(route, pathList, pathMap, parent) {
const path = parent ? `${parent.path}/${route.path}` : route.path;
const record = {
path,
component: route.component,
parent,
};
if (!pathList.includes(path)) {
pathList.push(path);
pathMap[path] = record;
}
if (route.children) {
route.children.forEach((o) => {
addRouteRecord(o, pathList, pathMap, record);
});
}
}</code></pre>
<p>你会发现<code>addRouteRecord</code>多了个<code>parent</code>参数,是因为有子路由。给<code>record</code>增加<code>parent</code>属性是因为方便后面父子组件递归渲染。</p>
<p><code>match</code> 实现完了,就该实现下<code>addRoute</code>。那么<code>addRoute</code>又该怎么实现呢?其实非常的简单,就是给<code>pathList</code> <code>pathMap</code>增加<code>routes</code>对象罢了。那么我们就开始动手吧!</p>
<pre><code class="js">// create-matcher.js
function addRoute(routes) {
createRouteMap(routes, pathList, pathMap);
}</code></pre>
<p>你会发现这里的<code>createRouteMap</code>增加了2个参数,所以<code>createRouteMap</code>方法就需要去兼容了。那么我们就开始动手吧!</p>
<pre><code class="js">// create-route-map.js
function createRouteMap(routes, oldPathList, oldPathMap) {
- const pathList = [];
+ const pathList = oldPathList || [];
- const pathMap =Object.create(null);
+ const pathMap = oldPathMap || Object.create(null);
// ...
}</code></pre>
<p>只需要对原先创建<code>pathList</code> <code>pathMap</code>对象进行兼容即可。</p>
<h4>History</h4>
<p><code>constructor</code>中的<code>matcher</code>对象创建讲完之后,接下来我们来讲讲<code>constructor</code>中的<code>HashHistory</code>。</p>
<p><code>HashHistory</code>该怎么实现?<code>new HashHistory</code>后做了什么呢?</p>
<p>因为<code>history</code>模式有多种,我们需要实现一个<code>base</code>版的<code>history</code>,然后<code>HashHistory</code>基于这个基类进行扩展。</p>
<pre><code class="js">// history/base.js
export default class History {
constructor() {}
}</code></pre>
<pre><code class="js">import History from './base';
export default class HashHistory extends History {
constructor(router) {
super(router);
this.router = router;
}
}</code></pre>
<h4>init</h4>
<h5>init 哪里调用</h5>
<p>这个<code>init</code>会在哪里调用呢?当然是安装的时候调用。所以我们在<code>install</code>的时候加上<code>init</code>的调用。</p>
<p><code>init</code>为什么要传入根实例呢?因为需要监听当前<code>url</code>对应的路由变化。当他变化之后,会主动将根实例的<code>_route</code>赋值成当前的根路由。那根实例的<code>_route</code>哪来的呢?可能有人忘记了。可以翻到上面<code>install</code>的章节,那里讲过了。</p>
<pre><code class="js">// install.js
const install = (Vue) => {
Vue.mixin({
beforeCreate() {
if (this.$options.router) {
this._routerRoot = this;
this._router = this.$options.router;
+ this._router.init(this)
} else {
this._routerRoot = this.$parent && this.$parent._routerRoot;
}
},
});
}</code></pre>
<p>加号位置就是我添加的代码。调用下<code>init</code>初始化下<code>VueRouter</code></p>
<p>那么<code>init</code>方法中做了哪些事情呢?</p>
<ul>
<li>挂载当前<code>url</code>对应的路由组件</li>
<li>监听路由的变化(<code>hashchange</code>等事件)</li>
</ul>
<p>话不多说,进入编码模式</p>
<pre><code class="js">// vue-router/index.js
import HashHistory from './history/hash';
import install from './install';
import createMatcher from './create-matcher';
class VueRouter {
constructor(options) {
// ...
}
init(app) {
const history = this.history;
const setupListener = () => {
history.setupListener();
};
history.transitionTo(history.getCurrentLocation(), setupListener);
history.listen((route) => {
app._route = route;
});
}
}
</code></pre>
<h5>init中做了哪些工作</h5>
<h6>transitionTo</h6>
<p><code>init</code>中调用了<code>history</code>中的<code>transitionTo</code>,那么这个方法是干嘛的呢?用于路由的跳转,根据传入的<code>path</code>从<code>pathMap</code>中筛选出对应的<code>route</code>,这个方法会触发下面讲到的<code>listen</code>方法。这个方法触发后,会修改根实例的<code>_route</code>。修改之后,<code>router-view</code>就会响应式的改变,以达到刷新路由渲染页面的目的。因为调用<code>transitionTo</code>方法会有多种途径。一种是主动调用<code>push</code>方法等,需要主动修改浏览器地址栏<code>hash</code>值,一种是页面初始化调用,这个时候又需要监听<code>hashchange</code>等事件,所以<code>transitionTo</code>增加第二个参数用于回调。这样每个调用<code>transitionTo</code>后,可执行自己的逻辑。</p>
<p>话不多说上代码</p>
<pre><code class="js">// history/base.js
export default class History {
constructor(router) {
+ this.router = router
}
+ transitionTo(location, callback) {
+ const r = this.router.match(location)
+ console.log(r)
+ callback && callback()
+ }
}</code></pre>
<p>上面简短的代码是不是就实现了上面描述中的几个功能了。<br>还有两点功能没实现。</p>
<ul>
<li>怎么响应式刷新</li>
<li>怎么实现匹配路由(<code>match</code>)</li>
</ul>
<p>我们先来讲<strong>怎么响应式刷新</strong>。那么怎样才能实现<code>router-view</code>响应式的刷新呢?我们根据倒推的模式,<code>router-view</code>是根据根实例的<code>_route</code>做刷新的。所以需要增加个<code>current</code>对象用来表示当前路由。上代码🐶</p>
<pre><code class="js">// history/base.js
export default class History {
constructor(router) {
this.router = router
+ this.current = createRoute(null, {
+ path: '/'
+ })
}
transitionTo(location, callback) {
const r = this.router.match(location)
+ this.current = r
callback && callback()
console.log(r)
}
}</code></pre>
<p>可是光设置<code>current</code>还不能实现<code>router-view</code>响应式刷新。因为<code>route-view</code>是根据<code>$route</code>做响应式的。还记得在<code>install</code>的时候设置过<code>$route</code>吗?我们将它修改下。</p>
<pre><code class="js">// install.js
const install = (Vue) => {
Vue.mixin({
beforeCreate() {
if (this.$options.router) {
// ...
this._router.init(this)
+ Vue.util.defineReactive(this, '_route', this._router.history.current);
} else {
this._routerRoot = this.$parent && this.$parent._routerRoot;
}
},
});
Object.defineProperty(Vue.prototype, '$route', {
get() {
- return 'this is $route';
+ return this._routerRoot._route;
},
});
}</code></pre>
<p>这样 我们一旦修改<code>current</code>,页面的<code>$route</code>就会响应式更新了。刷新下页面试试看吧🐶。</p>
<p>竟然报错了。提示<code>createRoute is not defined</code>。</p>
<p>抄下上面贴过的代码</p>
<pre><code class="js">// history/base.js
export default class History {
constructor(router) {
this.router = router
+ this.current = createRoute(null, {
+ path: '/'
+ })
}
transitionTo(location, callback) {
const r = this.router.match(location)
+ this.current = r
callback && callback()
console.log(r)
}
}</code></pre>
<p>我们来讲讲<code>createRoute</code>的作用。他的作用是将匹配到的<code>route</code>进行处理,返回个包含<code>path</code>、<code>matched</code>字段。<code>matched</code>字段包含了,从匹配到的一级路由一直到最后一级路由。<code>router-view</code>也是根据这一数组进行父组件到子组件的渲染的。<code>match</code>方法中也用到的这个方法。下面再讲<code>match</code>方法。接下来我们就来实现<code>createRoute</code>方法。</p>
<pre><code class="js">// history/base.js
export function createRoute(record, location) {
const matched = [];
while (record) {
matched.unshift(record);
record = record.parent;
}
return {
matched,
...location,
};
}</code></pre>
<p>第一个参数<code>record</code>其实就是上文<code>createRouteMap</code>中<code>addRouteRecord</code>使用到的<code>record</code>。其中包含了<code>parent</code>字段就是在这个时候用到的。所以<code>/about/a</code>就可以生成<code>matched: [{path: '/about', component: componentAbout}, {path: '/about/a', component: componentAboutA}]</code>了。再次提醒下,<code>matched</code>用于<code>router-view</code>的层层渲染。话不多说,上代码🐶</p>
<pre><code class="js">// vue-router/index.js
class VueRouter {
constructor(options) {
this.matcher = createMatcher(options.routes);
// ...
}
match(location) {
return this.matcher.match(location);
}
// ...
}</code></pre>
<p>提醒下,这里的<code>matcher</code>上面讲过了。用来返回<code>match</code>跟<code>addRoute</code>方法。</p>
<p>然后再完善下上面写过的<code>createMatcher</code></p>
<pre><code class="js">// create-matcher.js
+ import { createRoute } from './history/base';
function createMatcher(routes) {
const { pathList, pathMap } = createRouteMap(routes);
// ...
function match(locaiton) {
- return pathMap[locaiton];
+ return createRoute(pathMap[locaiton], {
+ path: locaiton,
+ });
}
// ...
}</code></pre>
<p>看着好像没什么问题了,再刷新下页面。</p>
<p>提示<code>history.setupListener is not a function</code></p>
<p>我们再把思绪拉回到<code>init</code>方法中调用的<code>history.setupListener</code></p>
<p><code>setupListener</code>其实非常的简单,就是添加监听事件</p>
<pre><code class="js">export default class HashHistory extends History {
constructor(router) {
super(router);
this.router = router;
}
getCurrentLocation() {
return window.location.hash.slice(1);
}
+ setupListener() {
+ window.addEventListener('hashchange', () => {
+ this.transitionTo(this.getCurrentLocation());
+ });
+ }
}</code></pre>
<p>监听到<code>hashchange</code>后,主动调用下<code>transitionTo</code>去切换路由。</p>
<p>然后我们再刷新下页面,又报错 excuse me???</p>
<p>提示: <code>TypeError: history.listen is not a function</code></p>
<h6>listen</h6>
<pre><code class="js">class VueRouter {
constructor(options) {
// ...
this.history = new HashHistory(this);
}
init(app) {
const history = this.history;
// ...
history.transitionTo(history.getCurrentLocation(), setupListener);
> history.listen((route) => {
> app._route = route;
> });
}
}</code></pre>
<p><code>init</code>中<code>history.listen</code>其实也是非常的简单,就是添加订阅。当调用<code>transitionTo</code>后,触发下订阅的事件。并传入<code>location</code>对应的<code>route</code>。话不多说,上代码🐶</p>
<pre><code class="js">// history/base.js
export default class History {
transitionTo(location, callback) {
const r = this.router.match(location);
// ...
this.cb && this.cb(r)
}
listen(cb) {
this.cb
}
}</code></pre>
<p>这个时候再刷新下页面。打印下<code>.vue</code>文件里的<code>this.$route</code>看了下好像没问题😆。</p>
<p>上文说过<code>router-view</code>是根据<code>$route</code>层层渲染的,既然<code>$route</code>都冇问题了,那就开始编写<code>router-view</code>组件吧。</p>
<h4>router-view</h4>
<p>在我看<code>router-view</code>源码之前,我根本不知道<code>router-view</code>怎么实现。原来它是根据<code>$route</code>的<code>matched</code>匹配到的组件进行层层渲染的。<br>举个🌰,就举上面打印出来的<code>matched</code>来讲好了,<code>app.vue</code>中的<code>router-view</code>会渲染<code>matched</code>的第一项中的<code>component</code>对应的<code>about.vue</code>组件,<code>about.vue</code>中的<code>router-view</code>会渲染<code>matched</code>中第二个<code>component</code>对应的<code>about/a</code>组件。可能上代码更简单易懂🐶,那我就开始开发了(本文都是边开发边写文章的)。</p>
<p><code>router-view</code>组件是用的函数式组件,因为<a href="https://link.segmentfault.com/?enc=Q11iEduJt6g95Lt%2BAejqng%3D%3D.v6Wf2VHDA5Cyw8oaYdOkCbS%2F3IOz4KQNmwJzDhFVLr2ng%2F9mlRCC%2B42oOxJFaaCB4M5PP%2FloD86I3rdODbaI8%2FERsty%2BPFlYw2YzQe6%2FU4Exnbjrg82LvOzk9%2F66EhCQ4FeOGBwG00QfV5o6g3qB3g%3D%3D" rel="nofollow">函数式组件</a>无状态 (没有响应式数据),也没有实例 (没有 this 上下文)</p>
<pre><code class="js">// components/router-view.js
export default {
functional: true,
render(h, {parent, data}) {
console.log(parent, 'parent')
}
}</code></pre>
<p>修改<code>install</code>中<code>rotuer-view</code>组件定义。</p>
<pre><code class="js">// install.js
import RouterView from './components/router-view';
const install = (Vue) => {
- Vue.component('router-view', {
- render() {
- return <div>this is router-view</div>;
- },
- });
+ Vue.component('router-view', RouterView);
};
export default install;</code></pre>
<p>上面代码写完之后,好像一切正常,打印出来也是正常的。接下来就根据我上面说的,他是根据<code>$route.matched</code>来渲染的去实现它吧。</p>
<pre><code class="js">export default {
functional: true,
render(h, { parent, data }) {
console.log(parent, 'parent');
const route = parent.$route;
let depth = 0;
// 判断是否渲染过,如果没有渲染过,就渲染对应matched里的组件,并将该组件data.routerView = 1。以达到不会重复渲染。
while (parent) {
if (parent.$vnode && parent.$vnode.data.routerView) {
depth++;
}
parent = parent.$parent;
}
data.routerView = 1;
const record = route.matched[depth];
if (!record) {
return h();
}
return h(record.component, data);
},
};</code></pre>
<p><code>while</code>的作用是判断是否渲染过,如果没有渲染过,就渲染对应matched里的组件,并将该组件data.routerView = 1。以达到不会重复渲染。刷新下页面看看吧。好像子页面都出来了。</p>
<h4>router-link</h4>
<p><code>router-link</code>非常的简单。我这里就实现下比较常见的一些操作。比如参数<code>tag</code>,<code>to</code>等</p>
<pre><code class="js">export default {
props: {
tag: {
type: String,
default: 'a',
},
},
render() {
const tag = this.tag;
return <tag>{this.$slots.default}</tag>;
},
};</code></pre>
<p>我这里先写个比较简单的架子。非常的简单将文字展示在<code>tag</code>中</p>
<p>然后再修改下<code>install</code>中的引入</p>
<pre><code class="js">// install.js
import RouterLink from './components/router-link';
const install = (Vue) => {
- Vue.component('router-link', {
- render() {
- return <div>this is router-link</div>;
- },
- });
+ Vue.component('router-link', RouterLink);
};
export default install;</code></pre>
<p>刷新下页面试试。页面一切展示正常🐶。这就大功告成了吗?当然没有啦,还有<code>router-link</code>的点击事件没写,那就开始动手吧。</p>
<pre><code class="js">// router-link.js
export default {
props: {
tag: {
type: String,
default: 'a',
},
+ to: {
+ type: String,
+ required: true,
+ },
},
+ methods: {
+ handler() {
+ this.$router.push(this.to);
+ },
+ },
render() {
const tag = this.tag;
- return <tag>{this.$slots.default}</tag>;
+ return <tag onClick={this.handler}>{this.$slots.default}</tag>;
},
};</code></pre>
<p>给<code>tag</code>增加点击事件。我上面的例子将<code>to</code>定义成<code>String</code>类型了。其实这个<code>to</code>可以是对象类型。我的例子只要能体现出<code>router-link</code>的功能就行了。</p>
<p>接下来刷新下页面试试看。</p>
<p>报了个错: <code>TypeError: this.$router.push is not a function</code></p>
<p>原来我<code>push</code>忘记写了。那就在<code>vue-router</code>实例中加一个吧。</p>
<pre><code class="js">// vue-router/index.js
class VueRouter {
constructor(options) {
// ...
this.history = new HashHistory(this);
}
+ push(location) {
+ this.history.transitionTo(location, () => {
+ window.location.hash = location;
+ });
+ }
}</code></pre>
<p>调用下<code>history.transitionTo</code>进行路由的匹配替换,触发<code>router-view</code>渲染后,还有在回调中主动修改下<code>hash</code>地址。</p>
<p>刷新下页面。一切正常。再点击<code>router-link</code>标签。emmmm 一切正常。(重复点击一个link跳转没有处理,我就不处理。我觉得没有必要,我的目的就是能表达我对router的理解就够了)</p>
<h3>最后</h3>
<p>非常感谢你能读完这篇文章。我已经非常努力写这篇文章了(vue-router写了三遍才开始写文章的)。但是读起来好像还是不是很顺。</p>
docker从零搭建jenkins服务器
https://segmentfault.com/a/1190000022236729
2020-04-02T04:24:35+08:00
2020-04-02T04:24:35+08:00
我是南方大汉
https://segmentfault.com/u/woshinanfangdahan
0
<h2>前言</h2>
<p>写这篇文章的灵感来源于最近公司的 jenkins 部署老是失败,各种原因。在项目非常赶的情况下,我每天还要抽半天时间去排查 jenkins 上的问题。所以决定在我们前端服务器上搭建个 jenkins 给测试部署。并部署到前端服务器上。文章是边操作边写出来的,踩遍了坑。不知道大家会不会也遇到这些问题。反正我都把解决步骤写在里面了。</p>
<p>本文主要内容是介绍 jenkins 的搭建与使用。至于是安装在服务器上还是本文通过 docker 安装 jenkins 不是很重要,默认读者会使用 docker。如果不是很了解<code>docker</code>可看我的<a href="https://link.segmentfault.com/?enc=My2g3oIOCSwxPEx3T5Sxfg%3D%3D.KyK2VfRVgSAz0nllNIGuqFd2UQQeFNvPrC7cH1lGhtOU6tm2OJqjqtfEfdlHai9BHmDPA4AGqCqT60jwXVBqxTkM35cSo%2BogZk8O99mtpKALv2qolO%2BrrnGpjihipyhNA4uuYHALAvNGpQpnqWwZmICKrseoP98SKedNdXAHgkaAZ9CKR1zE1pkbqBn2J3sF" rel="nofollow">docker 从入门到实战</a>博客,本文章同步发表于<a href="https://link.segmentfault.com/?enc=h8kk86Uj2rtmb3Z8%2FiuktQ%3D%3D.HQrLyiaM1sTrRsEHy%2Ffiouxj9AKGIvzFNNfg5qgd1oorzL7ARpbJ9NRXkEz7an%2Fo" rel="nofollow">我的博客</a></p>
<h2>安装 jenkins</h2>
<p><code>docker pull docker.io/jenkins/jenkins:latest</code></p>
<p>安装成功后使用<code>docker images</code>查看镜像</p>
<pre><code class="shell">github docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
jenkins/jenkins latest 59f8784e08ee 2 days ago 619MB</code></pre>
<p>在启动 Jenkins 前,需要先创建一个 Jenkins 的配置目录,并且挂载到 docker 里的 Jenkins 目录下</p>
<p>新建一个文件夹如我的<code>/Users/zhouatie/Desktop/github/front-end/practise-jenkins</code></p>
<p>并给该文件夹授权<code>sudo chown -R 1000 /Users/zhouatie/Desktop/github/front-end/practise-jenkins</code></p>
<blockquote>这里有个很神奇的点就是网上都说要授权,所以我授权了,但是还是提示<code>Can not write to /var/jenkins_home/copy_reference_file.log. Wrong volume permissions</code>。后来我新建了个文件夹不做授权处理就可以了。可查阅<a href="https://link.segmentfault.com/?enc=VQVSWVViu81h79uNHQ4QzQ%3D%3D.TOjURlz0Zl%2Bt4YFU8PRvROn31Zpwnih8ezFtzUkYyKkMcnXaQRaF5x46lV8sbhQx" rel="nofollow">stackoverflow</a> 这里面一位朋友就是遇到相同的问题 授权了也没用。</blockquote>
<p>执行以下命令构建容器</p>
<pre><code class="shell">docker run -itd -p 8080:8080 -p 50000:50000 --name jenkins -v /Users/zhouatie/Desktop/github/front-end/practise-jenkins:/var/jenkins_home docker.io/jenkins/jenkins:latest
</code></pre>
<p>执行<code>docker ps</code>查看后台启动的容器情况</p>
<pre><code class="shell">➜ front-end git:(master) ✗ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
3385ad0e42fe jenkins/jenkins:latest "/sbin/tini -- /usr/…" 5 minutes ago Up 5 minutes 0.0.0.0:8080->8080/tcp, 0.0.0.0:50000->50000/tcp jenkins</code></pre>
<p>可以看到 jenkins 容器已经启动成功了。这个时候访问下页面吧。浏览器输入 <code>localhost:8080</code></p>
<p>可以看到如下界面</p>
<p><img src="/img/remote/1460000022236753" alt="图片" title="图片"></p>
<p>这个时候我们就可以到刚才管理数据卷的文件夹里找了(我本地是<code>/Users/zhouatie/Desktop/github/front-end/practise-jenkins/secrets/initialAdminPassword</code>),<code>cat</code>下这个文件可以看到输出<code>28023d3751214bd6aadc0dd83c168325</code>,把这个密码复制到管理员密码输入框中并点击继续。</p>
<p>loading 转了半天,有种不详的预感。结果不出意外显示 jenkins 离线。所以我又开始上网搜<a href="https://link.segmentfault.com/?enc=tUZ4PKGclh2uvnjTIYndnA%3D%3D.1dAvj9EvgdOLVuIl1aAJQW8h34T%2BpLeEFET9j%2BnwNkGI4kTcoZ6QEHSI9GWnV0Y%2F" rel="nofollow">新版本 jenkins 安装时显示离线问题</a></p>
<p>解决步骤</p>
<ol>
<li>浏览器输入<code>http://localhost:8080/pluginManager/advanced</code><p>划到最下面可以看到</p>
<p><a href="https://link.segmentfault.com/?enc=qrmh7nKskh76nDNNbHOnRg%3D%3D.XCvv0CVg%2F4d7ADgKhNWhA3baEqMPFFNWaiFpsBPaEnA%3D" rel="nofollow"><img src="/img/remote/1460000022236737" alt="G8zfbR.md.jpg" title="G8zfbR.md.jpg"></a></p>
</li>
<li>将截图中的地址替换为<code>http://mirrors.tuna.tsinghua.edu.cn/jenkins/updates/current/update-center.json</code>这个代理服务器</li>
</ol>
<p>再刷新下<code>http://localhost:8080</code>页面如下</p>
<p><img src="/img/remote/1460000022236733" alt="G8zvVI.jpg" title="G8zvVI.jpg"></p>
<ol><li>
<p>点击安装推荐的插件</p>
<p>结果不出意外如下图</p>
<p><img src="/img/remote/1460000022236735" alt="GGpocD.jpg" title="GGpocD.jpg"></p>
<p>然后我又开始<code>google</code>了。找到了这个<a href="https://link.segmentfault.com/?enc=Q9QfacVFRn70Fz6Et9xb7g%3D%3D.ZHPuoejqB058aM8Nd%2BYnIdfTDNRpoaqc9IrDa3wJm6HUc4Bh%2BYnWbokMoBXzSL5tS8qeZ2iK8EHnEgyRMfyZ2g%3D%3D" rel="nofollow">安装 jenkins 时出现 No such plugin: cloudbees-folder 的解决办法</a></p>
<p>结果按照前几个步骤失败,作者建议重启,好吧,重启就重启,执行下 <code>docker restart jenkins</code></p>
<p>终于又成功进入下载页了。</p>
<blockquote>安装真的超级慢,心急如焚,不知道是不是 docker 的原因。因为文章是直接边操作边写的。在想要不要直接跨过这个安装界面,直接打开自己跑在前端服务器上的 jenkins 了开始介绍了</blockquote>
</li></ol>
<p><img src="/img/remote/1460000022236736" alt="GGpdkq.jpg" title="GGpdkq.jpg"></p>
<p>还没等他提示完全失败,这个时候我就又开始<code>google</code>了。实在没辙了,这的太难了。所以我重启了个<code>jenkins</code>容器后,选择自选插件。然后什么也不选,进了页面后,可以在如下截图地方下载,我是将上面推荐的全部勾选后进行下载,结果还很快。</p>
<p><img src="/img/remote/1460000022236734" alt="GGCv6S.jpg" title="GGCv6S.jpg"></p>
<ol><li>安装完插件后重启下,可以看到界面如下<p><img src="/img/remote/1460000022236732" alt="GGPpwj.jpg" title="GGPpwj.jpg"></p>
</li></ol>
<h2>构建</h2>
<ol><li>点击右上角的新建任务</li></ol>
<p><img src="/img/remote/1460000022236754" alt="图片" title="图片"></p>
<ol><li>选择第一个自由风格模式</li></ol>
<p><img src="/img/remote/1460000022236755" alt="tupian" title="tupian"></p>
<ol><li>确点后进入如下页面,并点击配置</li></ol>
<p><img src="/img/remote/1460000022236756" alt="图片" title="图片"></p>
<ol><li>因为部分插件装失败了,我就以在装成功的 jenkins 配置界面截图为例</li></ol>
<p><img src="/img/remote/1460000022236757" alt="t" title="t"></p>
<ul>
<li>
<strong>参数化构建</strong>:这里主要提下参数化构建,这里对应的值都可以在下面【构建】执行 shell 中获取到部署的时候用户手动选择或者填入的参数。</li>
<li>
<strong>源码管理</strong>:主要是让 jenkins 从你的 git 仓库中拉代码,<code>credentials</code>需要选择有该仓库权限的账号,可以手动试下</li>
<li>
<strong>构建触发器</strong>:意思就是触发条件,比如<code>git</code>上的<code>webhook</code>,就可以触发<code>jenkins</code>部署。具体可查阅<code>google</code>
</li>
<li>
<strong>构建</strong>:这个是重点,这里可以执行你的脚本,比如你是一个 vue 项目,可以根据上面配置的参数化配置,获取是否需要安装依赖等。可看我图中<code>shell</code>脚本,非常好理解。</li>
</ul>
命令行翻译插件
https://segmentfault.com/a/1190000021111666
2019-11-25T22:10:13+08:00
2019-11-25T22:10:13+08:00
我是南方大汉
https://segmentfault.com/u/woshinanfangdahan
0
<h2>@zhouatie/fanyi</h2>
<h3>说明</h3>
<p>平时开发的时候,遇到不会的单词,总要打开 一个翻译软件,或者浏览器翻译插件去查。所以就想开发一个命令行翻译插件,并发布到npm库上供大家使用。如果有帮助到你的话,可以给我<a href="https://link.segmentfault.com/?enc=PC7a8yPTD6U2MrgBv51khA%3D%3D.AHTq46U%2FGZFTQGTtZ940yO30tCmyGnFXmlHHQibDcPq3RCkaxQd8dfxA7xMXWPQPSYzWV%2FyeLXMFhnBbQWKBWA%3D%3D" rel="nofollow">仓库</a>来个star😆</p>
<h3>安装</h3>
<p><code>npm i @zhouatie/fanyi -g</code></p>
<h3>使用</h3>
<h4>翻译英文</h4>
<pre><code class="shell"># 翻译单词apple的意思
fanyi apple
----------------------
英 [ˈæpl] 美 [ˈæpl]
n. 苹果,苹果树,苹果似的东西;[美俚]炸弹,手榴弹,(棒球的)球;[美俚]人,家伙。</code></pre>
<pre><code class="shell"># 翻译句子
fanyi my name is bob
---------------
我叫鲍勃
</code></pre>
<h4>翻译中文</h4>
<pre><code class="shell">fanyi 苹果
----------
苹果 [píng guǒ]
[园艺] apple</code></pre>
<pre><code class="shell">fanyi 我是前端工程师
-----------
I'm a front end engineer</code></pre>
<h3>TODO</h3>
<ul>
<li>[x] 支持句子翻译</li>
<li>[ ] 加入commonader</li>
<li>[ ] 支持选择多个翻译平台</li>
</ul>
手写Promise
https://segmentfault.com/a/1190000020371464
2019-09-12T14:19:48+08:00
2019-09-12T14:19:48+08:00
我是南方大汉
https://segmentfault.com/u/woshinanfangdahan
1
<h2>手写Promise</h2>
<pre><code class="javascript">class TPromise {
static PENDING = 'PENDING';
static RESOLVED = 'RESOLVED';
static REJECTED = 'REJECTED';
constructor (handler) {
// 下面队列用于存储方法
this.resolveQueues = []
this.rejectQueues = []
this.finallyQueues = []
this.state = TPromise.PENDING
handler(this._resolve.bind(this), this._reject.bind(this))
}
_resolve (val) {
// 因为promise是微任务,这里使用MutationObserver来模拟微任务
let observer = new MutationObserver(() => {
// 一旦状态机修改过状态,那么就无法再改变状态
if (this.state !== TPromise.PENDING) return;
this.state = TPromise.RESOLVED
this.value = val
let handler;
while ( handler = this.resolveQueues.shift()) {
handler(this.value)
}
while ( handler = this.finallyQueues.shift()) {
handler()
}
})
observer.observe(document.body, {
attributes: true
})
document.body.setAttribute('_promise', Date.now())
}
_reject (val) {
// 因为promise是微任务,这里使用MutationObserver来模拟微任务
let observer = new MutationObserver(() => {
// 一旦状态机修改过状态,那么就无法再改变状态
if (this.state !== TPromise.PENDING) return;
this.state = TPromise.REJECTED
this.value = val
let handler;
while ( handler = this.rejectQueues.shift()) {
handler(this.value)
}
while ( handler = this.finallyQueues.shift()) {
handler()
}
})
observer.observe(document.body, {
attributes: true
})
document.body.setAttribute('_promise', Date.now())
}
then (resolveHandler, rejectHandler) {
// 每一个then都是返回一个新的promise
return new TPromise((resolve, reject) => {
if (typeof resolveHandler === 'function') {
const newResolveHandler = (val) => {
let result = resolveHandler(val)
if (result instanceof TPromise) {
// 如果返回的结果是个promise实例的话,调用下面方法
result.then(resolve, reject)
} else {
resolve(result)
}
}
// promise每次resolve的时候,都会去执行resolveQueues队列中的所有方法
this.resolveQueues.push(newResolveHandler)
} else {
// promise每次resolve的时候,都会去执行resolveQueues队列中的所有方法
this.resolveQueues.push(resolve)
}
if (typeof rejectHandler === 'function') {
const newRejectHandler = (val) => {
let result = rejectHandler(val)
if (result instanceof TPromise) {
result.then(resolve, reject)
} else {
resolve(result)
}
}
this.rejectQueues.push(newRejectHandler)
} else {
this.rejectQueues.push(reject)
}
})
}
catch (rejectHandler) {
// catch 实质上是then的一种简写
return this.then(undefined, rejectHandler)
}
finally (finallyHandler) {
return new TPromise((resolve, reject) => {
if (typeof finallyHandler === 'function') {
const newFinallyHandler = () => {
const result = finallyHandler()
if (result instanceof TPromise) {
result.finally(() => {
if (this.state === TPromise.RESOLVED) resolve(this.value)
else if (this.state === TPromise.REJECTED) reject(this.value)
})
} else {
if (this.state === TPromise.RESOLVED) resolve(this.value)
else if (this.state === TPromise.REJECTED) reject(this.value)
}
}
this.finallyQueues.push(newFinallyHandler)
}
})
}
// all 只有当所有promise都成功返回时,才resolve
static all (arr) {
let i = 0
const resArr = []
return new TPromise((resolve, reject) => {
arr.forEach((it, index) => {
it.then(res => {
i++
resArr[index] = res
if (i >= arr.length) {
resolve(resArr)
}
}).catch(err => {
reject(err)
})
});
})
}
// race 返回第一个有结果的promise
static race (arr) {
return new TPromise((resolve, reject) => {
arr.forEach((it, index) => {
it.then(res => {
resolve(res)
}).catch(err => {
reject(err)
})
});
})
}
static resolve (val) {
return new TPromise((resolve, reject) => {
if (val instanceof TPromise) {
val.then(resolve, reject)
} else {
resolve(val)
}
})
}
// 注意reject 会原封不动的将参数返回
static reject (val) {
return new TPromise((resolve, reject) => {
reject(val)
})
}
}
</code></pre>
前端必须知道的nginx知识
https://segmentfault.com/a/1190000020250065
2019-09-01T00:56:10+08:00
2019-09-01T00:56:10+08:00
我是南方大汉
https://segmentfault.com/u/woshinanfangdahan
3
<h2>前端必须知道的nginx知识</h2>
<p>前端开发者进阶之路不得不学nginx。</p>
<h3>本文内容涉及</h3>
<ul>
<li>nginx命令</li>
<li>配置文件、配置解释</li>
<li>变量</li>
<li>日志</li>
<li>跨域</li>
<li>代理请求</li>
<li>location拦截详解</li>
<li>gzip</li>
<li>防盗链</li>
<li>反向代理、正向代理</li>
<li>负载均衡</li>
<li>缓存</li>
<li>rewrite</li>
</ul>
<h3>正文</h3>
<h4>命令</h4>
<ul>
<li>nginx -T 查看当前nginx最终的配置</li>
<li>nginx -t 检查配置文件是否有语法错误</li>
<li>nginx -s reload 向主进程发送信号,重新加载配置文件</li>
<li>nginx -s stop 快速关闭</li>
<li>nginx -s quit 等待工作进程处理完成后关闭</li>
</ul>
<h4>配置文件</h4>
<pre><code class="shell">user nginx; # 定义 Nginx 运行的用户
worker_processes 1; # 设置工作进程的数量
error_log /var/log/nginx/error.log warn; #nginx 错误日志
pid /var/run/nginx.pid; # nginx.pid存放的是nginx的master进程的进程号
# events模块中包含nginx中所有处理连接的设置
events {
#工作进程的最大连接数量 理论上每台nginx服务器的最大连接数为worker_processes*worker_connections worker_processes为我们再main中开启的进程数
worker_connections 1024;
}
# 提供http服务相关的一些配置参数
http {
# include 是个主模块指令,可以将配置文件拆分并引用,可以减少主配置文件的复杂度, 这里是加载一些文件类型
include /etc/nginx/mime.types;
# default_type 属于HTTP核心模块指令,这里设定默认类型为二进制流,也就是当文件类型未定义时使用这种方式
default_type application/octet-stream;
# 定义日志格式
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
# 定义本虚拟主机的访问日志
access_log /var/log/nginx/access.log main;
sendfile on; #开启高效文件传输模式,sendfile指令指定nginx是否调用sendfile函数来输出文件,对于普通应用设为 on,如果用来进行下载等应用磁盘IO重负载应用,可设置为off,以平衡磁盘与网络I/O处理速度,降低系统的负载。注意:如果图片显示不正常把这个改 成off
sendfile on;
tcp_nopush on;
keepalive_timeout 65;
#gzip模块设置
gzip on; #开启gzip压缩输出
gzip_min_length 1k; #最小压缩文件大小
gzip_buffers 4 16k; #压缩缓冲区
gzip_http_version 1.0; #压缩版本(默认1.1,前端如果是squid2.5请使用1.0)
gzip_comp_level 2; #压缩等级
limit_zone crawler $binary_remote_addr 10m; #开启限制IP连接数的时候需要使用
server {
listen 80; # 表示http监听的端口
server_name localhost; # 域名可以有多个,用空格隔开 当出现多个server监听的端口一样时,可通过server_name 做区分
# 有些指令可以支持正则表达式
location / { # 用于匹配URI,这里也可以用正则的方式匹配 ~ 表示大小写敏感 ~*表示大小写不敏感
root html; # 指定了静态文件的根目录
index index.html index.htm;
}
error_page 500 502 503 504 /50x.html; # 表示当在500 502 503 504 错误码的时候重定向到 /50x.html
location = /50x.html {
root html;
}
}
include /etc/nginx/conf.d/*.conf; # 用于加载其他配置文件进来
}</code></pre>
<h4>变量</h4>
<table>
<thead><tr>
<th>字段</th>
<th>说明</th>
</tr></thead>
<tbody>
<tr>
<td><code>$arg_PARAMETER</code></td>
<td>这个变量值为:<code>GET</code> 请求中变量名 PARAMETER 参数的值。</td>
</tr>
<tr>
<td><code>$args</code></td>
<td>这个变量等于 <code>GET</code> 请求中的参数。例如,<code>foo=123&bar=blahblah;</code>这个变量只可以被修改</td>
</tr>
<tr>
<td><code>$binary_remote_addr</code></td>
<td>二进制码形式的客户端地址。</td>
</tr>
<tr>
<td><code>$body_bytes_sent</code></td>
<td>传送页面的字节数</td>
</tr>
<tr>
<td><code>$content_length</code></td>
<td>请求头中的 <code>Content-length</code> 字段。</td>
</tr>
<tr>
<td><code>$content_type</code></td>
<td>请求头中的 <code>Content-Type</code> 字段。</td>
</tr>
<tr>
<td><code>$cookie_COOKIE</code></td>
<td>
<code>cookie COOKIE</code> 的值。</td>
</tr>
<tr>
<td><code>$document_root</code></td>
<td>当前请求在 <code>root</code> 指令中指定的值。</td>
</tr>
<tr>
<td><code>$document_uri</code></td>
<td>与 <code>$uri</code> 相同。</td>
</tr>
<tr>
<td><code>$host</code></td>
<td>请求中的主机头<code>(Host)</code>字段,如果请求中的主机头不可用或者空,则为处理请求的 <code>server</code> 名称(处理请求的 <code>server</code> 的 <code>server_name</code> 指令的值)。值为小写,不包含端口。</td>
</tr>
<tr>
<td><code>$hostname</code></td>
<td>机器名使用 <code>gethostname</code> 系统调用的值</td>
</tr>
<tr>
<td><code>$http_HEADER</code></td>
<td>
<code>HTTP</code>请求头中的内容,HEADER 为 HTTP 请求中的内容转为小写,-变为_(破折号变为下划线),例如:<code>$http_user_agent</code>(<code>Uaer-Agent</code> 的值), <code>$http_referer</code>...;</td>
</tr>
<tr>
<td><code>$sent_http_HEADER</code></td>
<td>
<code>HTTP</code>响应头中的内容,<code>HEADER</code>为<code>HTTP</code>响应中的内容转为小写,-变为_(破折号变为下划线), 例如:<code>$sent_http_cache_control, $sent_http_content_type</code>...;</td>
</tr>
<tr>
<td><code>$is_args</code></td>
<td>如果<code>$args</code> 设置,值为"?",否则为""。</td>
</tr>
<tr>
<td><code>$limit_rate</code></td>
<td>这个变量可以限制连接速率。</td>
</tr>
<tr>
<td><code>$nginx_version</code></td>
<td>当前运行的 <code>nginx</code> 版本号。</td>
</tr>
<tr>
<td><code>$query_string</code></td>
<td>与<code>$args</code> 相同。</td>
</tr>
<tr>
<td><code>$remote_addr</code></td>
<td>客户端的 IP 地址。</td>
</tr>
<tr>
<td><code>$remote_port</code></td>
<td>客户端的端口。</td>
</tr>
<tr>
<td><code>$remote_user</code></td>
<td>已经经过 <code>Auth Basic Module</code> 验证的用户名。</td>
</tr>
<tr>
<td><code>$request_filename</code></td>
<td>当前连接请求的文件路径,由 <code>root</code> 或 <code>alias</code> 指令与 <code>URI</code> 请求生成。</td>
</tr>
<tr>
<td><code>$request_body</code></td>
<td>这个变量(0.7.58+)包含请求的主要信息。在使用 <code>proxy_pass</code> 或 <code>fastcgi_pass</code> 指令的 <code>location</code> 中比较有意义。</td>
</tr>
<tr>
<td><code>$request_body_file</code></td>
<td>客户端请求主体信息的临时文件名。</td>
</tr>
<tr>
<td><code>$request_completion</code></td>
<td>如果请求成功,设为"OK";如果请求未完成或者不是一系列请求中最后一部分则设为空。</td>
</tr>
<tr>
<td><code>$request_method</code></td>
<td>这个变量是客户端请求的动作,通常为 GET 或 POST。包括 0.8.20 及之前的版本中,这个变量总为 main request 中的动作,如果当前请求是一个子请求,并不使用这个当前请求的动作。</td>
</tr>
<tr>
<td><code>$request_uri</code></td>
<td>这个变量等于包含一些客户端请求参数的原始 URI,它无法修改,请查看&dollar;uri 更改或重写 URI。</td>
</tr>
<tr>
<td><code>$scheme</code></td>
<td>所用的协议,比如<code>http</code>或者是<code>https</code>,比如<code>rewrite ^(.+)$ $scheme://example.com$1 redirect</code>;</td>
</tr>
<tr>
<td><code>$server_addr</code></td>
<td>服务器地址,在完成一次系统调用后可以确定这个值,如果要绕开系统调用,则必须在 <code>listen</code> 中指定地址并且使用 <code>bind</code> 参数。</td>
</tr>
<tr>
<td><code>$server_name</code></td>
<td>服务器名称。</td>
</tr>
<tr>
<td><code>$server_port</code></td>
<td>请求到达服务器的端口号。</td>
</tr>
<tr>
<td><code>$server_protocol</code></td>
<td>请求使用的协议,通常是 <code>HTTP/1.0</code> 或 <code>HTTP/1.1</code>。</td>
</tr>
</tbody>
</table>
<h4>日志</h4>
<h5>error_log</h5>
<p>表示错误日志。<code>error_log /var/log/nginx/error.log warn;</code> 。错误日志记录了访问出错的信息。有利于我们排查错误。</p>
<h5>access_log</h5>
<p>表示访问日志。需要指定日志格式 <code>log_format</code>。通过访问日志我们可以得到用户的IP地址、浏览器的信息,请求的处理时间等信息</p>
<h5>log_format</h5>
<p>日志格式可以结合上面的变量,来指定访问日志的输出格式。</p>
<p>如:</p>
<pre><code class="shell">log_format main '$host $document_uri $server_addr $remote_addr $remote_port';
server {
listen 5002;
access_log /usr/local/etc/nginx/servers/access.log main;
location / {
root /usr/local/etc/nginx/servers;
index index.html;
}
}</code></pre>
<p>定义了个日志格式 main</p>
<p>然后在将<code>access_log</code>绑定日志<code>main</code></p>
<p>输出:</p>
<pre><code class="shell">localhost /index.html 127.0.0.1 127.0.0.1 61517</code></pre>
<p>可以看到,access_log 按照我的main日志格式输出了。</p>
<h4>跨域</h4>
<p>相信作为前端开发者,你肯定知道CORS。</p>
<ul><li>Access-Control-Allow-Origin <origin> | *;</li></ul>
<p>其中,origin 参数的值指定了允许访问该资源的外域 URI。对于不需要携带身份凭证的请求,服务器可以指定该字段的值为通配符,表示允许来自所有域的请求。</p>
<ul><li>Access-Control-Expose-Headers: X-My-Custom-Header, X-Another-Custom-Header</li></ul>
<p>在跨域访问时,XMLHttpRequest对象的getResponseHeader()方法只能拿到一些最基本的响应头,Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma,如果要访问其他头,则需要服务器设置本响应头。</p>
<ul><li>Access-Control-Max-Age</li></ul>
<p>Access-Control-Max-Age 头指定了preflight请求的结果能够被缓存多久;Access-Control-Max-Age: 86400</p>
<ul><li>Access-Control-Allow-Credentials</li></ul>
<p>指定了当浏览器的credentials设置为true时是否允许浏览器读取response的内容。当用在对preflight预检测请求的响应中时,它指定了实际的请求是否可以使用credentials。请注意:简单 GET 请求不会被预检;如果对此类请求的响应中不包含该字段,这个响应将被忽略掉,并且浏览器也不会将相应内容返回给网页</p>
<p>话不多少,下面就直接贴代码:</p>
<pre><code class="javascript">// index.js
import axios from 'axios'
axios.get('http://127.0.0.1:5556/api/abc').then(res => {
console.log(res, 'res')
document.write(res)
})</code></pre>
<pre><code class="shell">server {
listen 5556;
location /api {
proxy_pass http://localhost:5000;
add_header Access-Control-Allow-Origin *;
}
}</code></pre>
<p>将5556端口的请求路径 /api 开头的 转发到 5000 端口的node服务上。添加上<code>Access-Control-Allow-Origin *</code>就可解决跨域问题。</p>
<h4>代理请求</h4>
<p>代理请求可以看下上面的跨域。把5556端口的请求转发到5000端口的服务上。</p>
<h4>location拦截详解</h4>
<h5>修饰符</h5>
<ul>
<li>
<code>=</code>: 精确匹配路径</li>
<li>
<code>~</code>: 表示用该符号后面的正则去匹配路径,区分大小写</li>
<li>
<code>~*</code>: 表示用该符号后面的正则去匹配路径,不区分大小写</li>
<li>
<code>^~</code>: 表示如果该符号后面的字符是最佳匹配,采用该规则,不再进行后续的查找。</li>
</ul>
<pre><code class="shell">location = / {
[ configuration A ]
}
location / {
[ configuration B ]
}
location /api/ {
[ configuration C ]
}
location ^~ /static/ {
[ configuration D ]
}
location ~* \.(gif|jpg|jpeg)$ {
[ configuration E ]
}</code></pre>
<p>请求/精准匹配A,不再往下查找。</p>
<p>请求/index.html匹配B。首先查找匹配的前缀字符,找到最长匹配是配置B,接着又按照顺序查找匹配的正则。结果没有找到,因此使用先前标记的最长匹配,即配置B。</p>
<p>请求/api/list 匹配C。首先找到最长匹配C,由于后面没有匹配的正则,所以使用最长匹配C。</p>
<p>请求/user/1.jpg匹配E。首先进行前缀字符的查找,找到最长匹配项C,继续进行正则查找,找到匹配项E。因此使用E。</p>
<p>请求/static/img.jpg匹配D。首先进行前缀字符的查找,找到最长匹配D。但是,特殊的是它使用了^~修饰符,不再进行接下来的正则的匹配查找,因此使用D。这里,如果没有前面的修饰符,其实最终的匹配是E。</p>
<p>请求/router/pageA 匹配B。因为B表示任何以/开头的URL都匹配。在上面的配置中,只有B能满足,所以匹配B。</p>
<h4>gzip</h4>
<h5>配置说明</h5>
<pre><code class="shell"># gzip 默认off 默认关闭gzip
gzip on;
# gzip_min_length 默认0
# 作用域: http, server, location
# 设置允许压缩的页面最小字节数,页面字节数从header头中的Content-Length中进行获取。
# 默认值是0,不管页面多大都压缩。
# 建议设置成大于1k的字节数,小于1k可能会越压越大。 即: gzip_min_length 1024
gzip_min_length 1k;
# gzip_comp_level 默认 1 范围 1 ~ 9
# 作用域: http, server, location
# gzip压缩比,1 压缩比最小处理速度最快,9 压缩比最大但处理最慢(传输快但比较消耗cpu)。
gzip_comp_level 6;
# 默认值: gzip_types text/html
# 作用域: http, server, location
# 匹配MIME类型进行压缩,(无论是否指定)"text/html"类型总是会被压缩的。
# 注意:如果作为http server来使用,主配置文件中要包含文件类型配置文件
gzip_types text/plain application/x-javascript text/css application/xml application/javascript application/json;</code></pre>
<h5>代码演示</h5>
<pre><code class="shell">server {
listen 9002;
#gzip on;
location / {
root /usr/local/etc/nginx/servers/;
index gzip.html;
}
}</code></pre>
<p>我们先记录下未开启 gzip 时加载的文件大小<br><img src="/img/remote/1460000022547929" alt="without-gzip" title="without-gzip"></p>
<p>size 显示的是 1.3kb</p>
<p>然后我们将 gzip 注释去掉的结果如下</p>
<p><img src="/img/remote/1460000022547930" alt="with-gzip" title="with-gzip"></p>
<p>可以看到只有300B了,当然你还可以根据其他配置,比如来控制压缩等级来控制输出的大小。我们前端项目打包的时候可以开启gzip,这样nginx就不用在服务器上进行gzip压缩了。</p>
<h4>防盗链</h4>
<pre><code class="shell">location ~ .*\.(jpg|png|gif)$ {
valid_referers none blocked 47.104.184.134;
if ($invalid_referer) {
return 403;
}
root /data/images;
}</code></pre>
<p>valid_referers none | blocked | server_names | string ....;<br>none 检测Referer头域不存在的请求<br>blocked 检测Referer头域的值被防火墙或者代理服务器删除或伪装的情况。<br>这种情况下,该头域的值不以“<a>http://</a>”或者“https://”开头<br>server_names 设置一个或多个URL,检测Referer头域的值是否是这些URL中的某个。<br>从nginx 0.5.33以后支持使用通配符“*”。</p>
<p><code>valid_referers</code> 用于支持访问该资源的referers</p>
<p>$invalid_referer 这个变量为true 表示不符合上面定义的规则。就return 403</p>
<h4>反向代理、正向代理</h4>
<h5>正向代理</h5>
<pre><code class="shell">location / {
proxy_pass http://$http_host$request_uri;
}</code></pre>
<p>正向代理你可以理解为代理客户端,比如VPN。因为国内无法访问国外的网站,所以通过将请求转发到VPN服务器,VPN将你的请求原封不动的转发到国外网址。正向代理,客户端知道服务端,服务端不知道客户端。</p>
<h5>反向代理</h5>
<p>比如说下面将会讲到的负载均衡。所有请求统一走到一个nginx服务上,由这个nginx服务讲请求分配到多台服务器上。</p>
<h4>负载均衡</h4>
<h5>代码演示</h5>
<pre><code class="shell">.
├── 9004.html
├── 9005.html
├── 9006.html
└── upstream.conf</code></pre>
<pre><code class="shell">server {
listen 9004;
location / {
root /usr/local/etc/nginx/servers/;
index 9004.html;
}
}
server {
listen 9005;
location / {
root /usr/local/etc/nginx/servers/;
index 9005.html;
}
}
server {
listen 9006;
location / {
root /usr/local/etc/nginx/servers/;
index 9006.html;
}
}
upstream atie {
server localhost:9004;
server localhost:9005;
server localhost:9006;
}
server {
listen 9003;
location / {
proxy_pass http://atie;
}
}</code></pre>
<p>通过上面代码可以看到我通过访问9003端口,均衡到其他端口。</p>
<p>打开浏览器访问 localhost:9003 可以看到页面会分别加载9004 ~ 9006.html 页面</p>
<h5>配置介绍</h5>
<table>
<thead><tr>
<th>状态</th>
<th>描述</th>
</tr></thead>
<tbody>
<tr>
<td>down</td>
<td>不参与负载均衡</td>
</tr>
<tr>
<td>backup</td>
<td>备份的服务器</td>
</tr>
<tr>
<td>max_fails</td>
<td>允许请求失败的次数</td>
</tr>
<tr>
<td>fail_timeout</td>
<td>经过max_fails失败后,服务暂停的时间</td>
</tr>
<tr>
<td>max_conns</td>
<td>限制最大的接收的连接数</td>
</tr>
<tr>
<td>weight</td>
<td>权重比</td>
</tr>
</tbody>
</table>
<pre><code class="shell">upstream atie {
server localhost:9004 down; # 这里如果加了down 表示负载均衡的时候就不会转到这个服务
server localhost:9005 backup; # 表示备份 只有当服务挂了,才会走到这个备份服务上
server localhost:9006 max_fails=1 fail_timeout=10s; # 超过最大次数后,在fail_timeout时间内,新的请求将不会分配给这台机器。
}</code></pre>
<pre><code class="shell">upstream atie {
server localhost:9004 weight=1;
server localhost:9005 weight=2;
server localhost:9006 weight=3;
}</code></pre>
<p>当你访问6次时,一次走到9004 2次走到9005 3次走到9006</p>
<h4>缓存</h4>
<p>通过添加缓存请求头设置过期时间等</p>
<pre><code class="shell">location ~ .*\.(gif|jpg|png|css|js)(.*) {
expires 90d; # 设置有效90天
}</code></pre>
<h4>rewrite</h4>
<p>rewrite功能就是,使用nginx提供的全局变量或自己设置的变量,结合正则表达式和标志位实现url重写以及重定向。rewrite只能放在server{},location{},if{}中,并且只能对域名后边的除去传递的参数外的字符串起作用</p>
<pre><code class="shell">location / {
rewrite /rewrite/(.*) http://www.$1.com;
return 200 "ok";
}
# 在浏览器中输入 127.0.0.1:8080/rewrite/google
# 则临时重定向到 www.google.com
# 后面的 return 指令将没有机会执行了</code></pre>
docker从入门到实战-实战篇
https://segmentfault.com/a/1190000019686588
2019-07-07T02:28:33+08:00
2019-07-07T02:28:33+08:00
我是南方大汉
https://segmentfault.com/u/woshinanfangdahan
0
<h2>docker从入门到实战-实战篇</h2>
<h3>前言</h3>
<p>本文是我通过三个星期业余时间学习后而写的文章,对docker的了解还处于入门阶段。希望本文能帮忙一些想学习docker的朋友快速入门。练习及实战代码都在github仓库中。如果我的文章能帮助到你的话,可以给我的<a href="https://link.segmentfault.com/?enc=1CHpSXqQMfgUpc26Q%2FsRQw%3D%3D.LRMzljImqChY28c8%2BQyKiw0E516cWwLbHaIsN78J8%2FUe%2FJ0B1YTOLMrrpTwx9LWP" rel="nofollow">docker项目</a>点个赞哦</p>
<h3>docker实战</h3>
<p>本次实战案例是todolist。技术栈为vue、node、mysql。具体代码见项目目录<a href="https://link.segmentfault.com/?enc=f5QFl2lo0x%2BnR5yDHX4pwA%3D%3D.8vmg83I4molAiGu9CAlHnbdC33f5ba%2Fv%2FG%2BwpIfFoPmsngVlcsjEcYAIFBtKXd6gZn1dVj9%2FXdMsLtCZllFLkw%3D%3D" rel="nofollow">todolist</a>,下面就不一一贴代码了。就讲下重点。</p>
<p>下面我就顺着依赖关系来讲,所以先从mysql开始讲起</p>
<h4>构建mysql</h4>
<p>执行:<code>docker run --name mymysql -d -p 3308:3306 -e MYSQL_ROOT_PASSWORD=123456 mysql</code></p>
<ul>
<li>--name 给mysql容器设置匿名</li>
<li>-d表示后台运行</li>
<li>-p表示将容器的3306端口的映射到本地的3308端口,如果不设置的话,本地是无法访问该MySQL服务的。</li>
<li>-e MYSQL_ROOT_PASSWORD 设置root的账号密码。</li>
<li>mysql 后面不指定版本话,默认会latest版本</li>
</ul>
<p>在执行该语句之前,假如你之前没有pull过mysql镜像。docker在本地找不到你要的镜像就会帮你从docker仓库拉取mysql:latest镜像。</p>
<p>这个时候容器就启动成功了。</p>
<p>尝试下用navicat连接下试试</p>
<p>鼠标放入黄色小三角出现如下报错。</p>
<pre><code class="javascript">2013 - Lost connection to MySQL server at 'reading initial communication packet', system error: 0 "Internal error/check (Not system error)"</code></pre>
<p>这是因为mysql8以上,都会使用新的验证方式。</p>
<p>不妨查下信息: <code>select host,user,plugin,authentication_string from mysql.user; </code></p>
<pre><code class="javascript">mysql> select host,user,plugin,authentication_string from mysql.user;
+-----------+------------------+-----------------------+------------------------------------------------------------------------+
| host | user | plugin | authentication_string |
+-----------+------------------+-----------------------+------------------------------------------------------------------------+
| % | root | caching_sha2_password | *6BB4837EB74329105EE4568DDA7DC67ED2CA2AD9 |
| localhost | mysql.infoschema | caching_sha2_password | $A$005$THISISACOMBINATIONOFINVALIDSALTANDPASSWORDTHATMUSTNEVERBRBEUSED |
| localhost | mysql.session | caching_sha2_password | $A$005$THISISACOMBINATIONOFINVALIDSALTANDPASSWORDTHATMUSTNEVERBRBEUSED |
| localhost | mysql.sys | caching_sha2_password | $A$005$THISISACOMBINATIONOFINVALIDSALTANDPASSWORDTHATMUSTNEVERBRBEUSED |
)7k44VulAglQJgGpvgSG.ylA/rdbkqWjiqQJiq3DGsug5HIy3 |ord | $A$005$0pU+sGm[on
+-----------+------------------+-----------------------+------------------------------------------------------------------------+</code></pre>
<p>plugin一栏可看到都是caching_sha2_password。</p>
<p>那么如何才能将其改成可连接的呢?只需要将其plugin改成mysql_native_password就可以访问了。</p>
<p><code>ALTER user 'root'@'%' IDENTIFIED WITH mysql_native_password BY '123456';</code></p>
<p>你可以先用上面查询账户信息查下是否修改成功了。</p>
<p>修改成功后,可以尝试下用navicat连接下mysql。不出意外的话就能成功连接上了。</p>
<p>当然我下面的例子用mysql:5.6,方便操作,不需要修改plugin。</p>
<p>执行命令:<code>docker run --name mymysql -d -e MYSQL_ROOT_PASSWORD=123456 -p 3308:3306 mysql:5.6</code></p>
<p>启动容器后可以执行:<code>docker exec -it mymysql bash</code>进入容器</p>
<p>执行:<code>mysql -uroot -p123456</code>进入mysql控制台</p>
<p>执行:<code>show databases;</code>查看mysql数据库</p>
<pre><code class="javascript">mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
+--------------------+
3 rows in set (0.00 sec)</code></pre>
<p>执行:<code>create database todolist;</code>创建todolist应用的数据库</p>
<p>执行:<code>show databases;</code>查看刚刚创建的todolist数据库</p>
<pre><code class="javascript">mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
| todolist |
+--------------------+
4 rows in set (0.00 sec)</code></pre>
<p>可以看到数据库中多了个todolist数据库</p>
<p>接下来选择该todolist数据库</p>
<p>执行:<code>use todolist;</code>选中该数据库</p>
<p>创建表:</p>
<pre><code class="javascript">CREATE TABLE list (
id INT(11) AUTO_INCREMENT PRIMARY KEY,
text VARCHAR(255),
checked INT(11) DEFAULT 0
);</code></pre>
<p>执行:<code>show tables;</code>查看todolist数据库下的表</p>
<pre><code class="javascript">mysql> show tables;
+--------------------+
| Tables_in_todolist |
+--------------------+
| list |
+--------------------+
1 row in set (0.00 sec)</code></pre>
<p>执行:<code>describe list;</code>查看表</p>
<pre><code class="javascript">mysql> describe list;
+---------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+---------+--------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| text | varchar(255) | YES | | NULL | |
| checked | int(11) | YES | | 0 | |
+---------+--------------+------+-----+---------+----------------+
3 rows in set (0.01 sec)</code></pre>
<p>执行:<code>insert into list set checked = 0, text = 'haha';</code> 往表中插入一条数据;</p>
<p>执行:<code>select * from list;</code></p>
<pre><code class="javascript">mysql> select * from list;
+----+------+---------+
| id | text | checked |
+----+------+---------+
| 1 | haha | 0 |
+----+------+---------+
1 row in set (0.01 sec)</code></pre>
<p>一切正常</p>
<h4>构建node</h4>
<p>mysql服务启动好了,接下来就是启动node服务,并连接刚启动的mysql服务了。</p>
<p>话不多说,直接上代码,解释看注释</p>
<pre><code class="javascript">// index.js
const mysql = require('mysql'); // mysql包
const express = require('express');
const app = express();
const bodyParser = require('body-parser'); // post请求需要引入的包
app.use(bodyParser.json());
// mysql配置(用于连接刚启动的mysql服务)
const opt = {
host: 'localhost',
user: 'root',
port: '3308',
password: '123456',
database: 'todolist'
};
const connection = mysql.createConnection(opt);
const sqlFn = (sql) => {
return new Promise((resolve, reject) => {
connection.query(sql, (err, results, filelds) => {
if (err) throw err;
resolve(results);
});
})
}
connection.connect(async (err) => {
if (err) throw err;
console.log('mysql connncted success!');
})
// todolist 列表查询
app.get('/getList', async (req, res) => {
const sql = `SELECT * FROM list`;
const data = await sqlFn(sql);
res.json({
code: 0,
data,
message: 'success'
})
})
// todolist 插入数据
app.post('/insert', async (req, res) => {
const sql = `INSERT INTO list SET checked = ${req.body.checked}, text = '${req.body.text}'`;
const data = await sqlFn(sql);
res.json({
code: 0,
data,
message: 'success'
})
})
app.listen(3000);
</code></pre>
<p>执行: <code>node index.js</code>后,控制台输入</p>
<pre><code class="javascript">➜ server git:(master) ✗ node index.js
mysql connncted success!</code></pre>
<p>表示node服务连接mysql服务成功;</p>
<p>浏览器可以访问下<code>localhost:3000/getList</code></p>
<pre><code class="javascript">{"code":0,"data":[{"id":1,"text":"haha","checked":0}],"message":"success"}</code></pre>
<p>页面将会出现刚才我们sql插入到数据库的数据</p>
<p>既然代码没有问题,那么我们接下来就把他构建成镜像。</p>
<p><strong>构建之前需要把代码中opt的host localhost改为自己主机的ip。因为容器启动的话,连接mysql需要通过主机的3308端口访问。</strong></p>
<p>在当前文件夹新建名为Dockerfile的文件</p>
<pre><code class="sh"># 基于最新的 node 镜像
FROM node:8
# 复制当前目录下所有文件到目标镜像 /app/ 目录下
COPY . /todolist/server
# 修改工作目录
WORKDIR /todolist/server
# 安装依赖
RUN ["npm", "install"]
# 启动 node server
ENTRYPOINT ["node", "index.js"]
</code></pre>
<p>执行:<code>docker build -t mynode .</code>,生成node镜像</p>
<ul><li>-t:表示给该镜像添加版本,这里不写默认为latest</li></ul>
<p>可通过<code>docker images</code>查看本地的镜像。</p>
<pre><code class="javascript">➜ server git:(master) ✗ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
mynode latest 3e8de2825063 4 seconds ago 898MB</code></pre>
<p>可以看到第一个镜像就是我们刚刚构建的镜像</p>
<pre><code class="javascript">➜ server git:(master) ✗ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
mynode latest 3e8de2825063 4 seconds ago 898MB</code></pre>
<p>接下来运行基于这个镜像的容器</p>
<p>执行:<code>docker run --name mynode -d -p 4000:3000 mynode</code></p>
<ul>
<li>--name 给node容器起一个容器匿名</li>
<li>-d 表示后台运行</li>
<li>-p 4000:3000 表示访问本地4000代理容器内的3000端口服务</li>
<li>mynode 是上面我们build构建的镜像</li>
</ul>
<p>启动成功后,访问下<code>localhost:4000/getList</code></p>
<pre><code class="javascript">{"code":0,"data":[{"id":1,"text":"haha","checked":0}],"message":"success"}</code></pre>
<p>可以看到页面输出我们上面执行sql语句插入的数据</p>
<p>上面贴的代码只是基础的查看列表与插入数据两个方法,其他的请参考<a href="https://link.segmentfault.com/?enc=60rMQ0Z0d9oQkPyGfLJV7A%3D%3D.hz5gfEFniE0F1NvUG5tG%2BEn1UAvGacfKwWxi76bVnSFGMqjDhum6Vw7k%2B%2BQhzals9xq9hbo%2FgVlASLZox1O64FFsP19dphgEhyVzG1L5S00%3D" rel="nofollow">server</a></p>
<h4>构建vue</h4>
<p>todolist的静态页面,我是通过vue-cli3搭建的。<br>执行<code>vue create app</code> 创建项目。</p>
<p>进入项目根目录执行<code>npm run serve</code>页面是否正常执行。</p>
<p>然后编写简易的具有增删改查的todolist应用。具体代码见<a href="https://link.segmentfault.com/?enc=s0kM1eBw7SgGipsiPnz%2Bow%3D%3D.csI2yqpsv%2BYDpwbv5LDHcDILAO%2FxUNpUYic0cPOktTl7uEw4%2FgCUZdPU1qStjerY%2FdKsPghjHnDi16LvaJY18Q%3D%3D" rel="nofollow">todolist</a>。</p>
<p><strong>注意vue.config.js中的devServer配置的target: '<a href="https://link.segmentfault.com/?enc=9XQTTbAGmBbptdbu88CVJw%3D%3D.9KOIfKvcQxKgdyKNhHmryH2hAoc5sfSE5kDDeNA5gWY%3D" rel="nofollow">http://127.0.0.1</a>:4000'代理到我们刚启动的node容器</strong></p>
<p>页面启动成功后,访问<code>localhost:8080</code></p>
<p>可以看到页面加载成功,列表也成功渲染出上面构建mysql时,sql插入的数据。</p>
<p>本地启动静态并请求服务端成功后,接下来也将静态页面打包成镜像,然后启动静态页面容器</p>
<p><strong>在打包镜像容器之前,记得先将vue.config.js中的devServer配置的target: '<a href="https://link.segmentfault.com/?enc=8oCNRcXz7mndyvAfz4lzaw%3D%3D.EPIPQCue2KrWMmQACiqkew%3D%3D" rel="nofollow">http://&lt</a>;主机ip地址>:4000'代理到我们刚启动的node容器</strong></p>
<p>编写Dockerfile</p>
<pre><code class="shell"># 基于最新的 node 镜像
FROM node:8
# 复制当前目录下所有文件到目标镜像 /app/ 目录下
COPY . /todolist/app
# 修改工作目录
WORKDIR /todolist/app
RUN npm config set registry https://registry.npm.taobao.org && npm install
# RUN ["npm", "install"]
# 启动 node server
ENTRYPOINT ["npm", "run", "serve"]</code></pre>
<p>cd到静态页面的根目录执行: <code>docker build -t static .</code></p>
<p>执行:<code>docker run --name static -d -p 9000:8080 static</code>启动静态容器</p>
<p>打开浏览器访问<code>localhost:9000</code>,可以看到页面成功渲染出列表页。</p>
<p>至此,mysql、node、vue容器均已互通。代码需要完善的地方详见<a href="https://link.segmentfault.com/?enc=Q1IFMFWLRCptjn%2FMXIgBSw%3D%3D.Ig5QT4%2BS%2BVYEsPr80yN7v1KX1BcIei%2BPp3V7p8j79nqs9%2FSxeIQEULoiHh3qUfeZCmMQjFAJtv2K05qlboSFgg%3D%3D" rel="nofollow">todolist</a></p>
<h4>docker-compose</h4>
<p>我们不可能每次部署一个应用,需要手动启动好几个服务。这个时候就需要使用<a href="https://link.segmentfault.com/?enc=KNWFWrcmQwPksFBIB8sieA%3D%3D.paICwcxYQwq0eO5zRVdm1Y0pLOF4%2Fr5v%2BizyumN%2FKIuTaABb7NK5nnTqmH4oam7GHBnI0uITjJWNp1Rp08QLc4X2EfNhaQKfsRaaev03fiA%3D" rel="nofollow">docker-compose</a></p>
<p>关于命令就不介绍了,这里贴下链接<br><a href="https://link.segmentfault.com/?enc=%2BCwJJhFr3qBPT3lcdDKzcA%3D%3D.dHohi4vMZpJsOp4wjSWs1eE6kWcN1T7FDmXWKdu5e5b3HXW8Ixs%2B%2BUMHh4uPYC0PVYHWf1Vx48ZeEdPHH0TKtA5k623S0Hh4m4yG%2Brtk46Q%3D" rel="nofollow">docker-compose命令</a></p>
<p>在根目录下新建<code>docker-compose.yml</code>配置文件</p>
<pre><code class="shell">version: '2'
services:
static:
build: ./app/
container_name: static
ports:
- 7001:8080
depends_on:
- nodejs
- db
nodejs:
build:
context: ./server/
dockerfile: sleep-dockerfile
container_name: nodejs
ports:
- 4000:3000
environment:
- IS_START_BY_COMPOSE=1
command: sh ./sleep.sh
depends_on:
- db
db:
image: mysql:5.6
container_name: db
environment:
MYSQL_ROOT_PASSWORD: "123456"
command:
--default-authentication-plugin=mysql_native_password
--character-set-server=utf8mb4</code></pre>
<p>具体配置,详见上面贴的链接。下面我介绍下我所写的配置</p>
<ul>
<li>version: docker-compose的版本</li>
<li>services: 代表你用docker-compose启动的几个服务</li>
<li>build: 指定 Dockerfile 所在文件夹的路径(可以是绝对路径,或者相对 docker-compose.yml 文件的路径)。 Compose 将会利用它自动构建这个镜像,然后使用这个镜像。</li>
<li>container_name: 指定容器名称。</li>
<li>ports: 相当于docker run启动容器的-p,<主机ip:容器ip>,这样主机通过这个端口去访问容器中的服务。</li>
<li>environment: 只给定名称的变量会自动获取运行 Compose 主机上对应变量的值,可以用来防止泄露不必要的数据。简而言之,就是在容器内可以获取给的环境变量。</li>
<li>depends_on: 解决容器的依赖、启动先后的问题。(但是它有个问题就是只是等所依赖的服务开启启动,并不是等依赖的服务启动成功后在构建当前的服务。</li>
<li>command: 覆盖容器启动后默认执行的命令</li>
</ul>
<p>接下来解释下上面用compose启动的服务</p>
<blockquote>值得一提的是,mysql的Dockerfile中做了创建数据库与表的操作。会涉及到关闭数据库密码登录功能。因为关闭之后,操作数据库就不需要输入密码了。等表建完之后在恢复密码。<br>nodejs中的sleep.sh脚本是因为depends_on这个依赖只是单纯的等待其他服务开始启动,并不是等待依赖的服务启动完成之后才开始构建自身的服务。这个时候node初始化会在mysql启动完成之前启动。这样的话,node启动的时候连接mysql就会报错,导致node服务挂掉。所以引用了sleep.sh,让node延迟一段时间启动。</blockquote>
<p>最后在<code>docker-compose</code>文件所在的目录下执行<code>docker-compose build</code></p>
<p>再执行:<code>docker-compose up</code>就可以启动todolist这个应用的所有服务了。</p>
docker从入门到实战-基础篇
https://segmentfault.com/a/1190000019686557
2019-07-07T02:22:07+08:00
2019-07-07T02:22:07+08:00
我是南方大汉
https://segmentfault.com/u/woshinanfangdahan
2
<h2>docker从入门到实战-基础篇</h2>
<h3>docker基础</h3>
<h3>前言</h3>
<p>本文是我通过三个星期业余时间学习后而写的文章,对docker的了解还处于入门阶段。希望本文能帮忙一些想学习docker的朋友快速入门。练习及实战代码都在github仓库中。如果我的文章能帮助到你的话,可以给我的<a href="https://link.segmentfault.com/?enc=waeNyR4tf%2BMaz03%2F1Jm8FA%3D%3D.PxwlvMMqQGKf0jbcej%2BH%2BWXtJs%2BTHdOVRp%2FITffu0dM76NUKGmj3eTaCZxv3U7f1" rel="nofollow">docker项目</a>点个赞哦</p>
<h4>介绍</h4>
<p>docker是一个开源的应用容器引擎,开发者可以打包自己的应用到容器里面,然后迁移到其他机器的docker应用中,可以实现快速部署。如果出现的故障,可以通过镜像,快速恢复服务。</p>
<p>举个例子,公司一般都会有多套环境,那么如何保持多套的运行环境一致,这个时候就可以用到docker。且当要求增加一套环境的时候,你无需在一个新服务器上一个个环境安装、配置。只需要运行下docker。同时官方还提供了<a href="https://link.segmentfault.com/?enc=u80itcJ1iUhvS%2BmKSxO9OQ%3D%3D.a2Zfrg%2FJpIM9P9ha5BHY8xptDQYpaYqjY5naB6Wsjjc%3D" rel="nofollow">Docker Hub</a>,拥有大量的高质量的官方镜像。你可以将自己的镜像上传上去。有点类似于<code>github</code>。</p>
<h4>安装</h4>
<p>官方提供了安装教程,挺详细的。<a href="https://link.segmentfault.com/?enc=9DYzWTR24vpT82SxHOOygg%3D%3D.Jon18eelsm%2BY2XSt5SH9q%2FRlSn5XCCBQuPMDabmL09oOKj9W7Ese0PkeXtbPPaha" rel="nofollow">官方安装教程</a></p>
<h4>docker起步</h4>
<p>第一步:执行下<code>docker -v</code>确认下是否成功安装了docker</p>
<p>如果成功安装了,命令行将会输出入docker的版本号。如下:<br><code>Docker version 18.09.2, build 6247962</code></p>
<p>docker的整个生命周期大致可分为:</p>
<ol>
<li>镜像</li>
<li>容器</li>
<li>仓库</li>
</ol>
<p>这里以<code>ubuntu</code>镜像为例,介绍下镜像</p>
<p>在下载<code>ubuntu</code>镜像之前运行下<code>docker images(查看镜像命令)</code>查看下本地的镜像。如果你还没下载过镜像的话,当然会出现空。这里贴下我本地的镜像</p>
<pre><code class="javascript">➜ study-docker git:(master) ✗ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
todolist_static latest de5e325037e9 2 hours ago 1.05GB
todolist_nodejs latest 53efd80e03e1 2 hours ago 898MB
ubuntu 18.04 7698f282e524 4 weeks ago 69.9MB
mysql latest 990386cbd5c0 5 weeks ago 443MB
node 8 a5c31320f223 6 weeks ago 895MB
mysql 5.6 73829d7b6139 6 weeks ago 256MB</code></pre>
<p>使用拉取镜像命令<code>docker pull</code> 拉取<code>ubuntu</code>镜像:<code>docker pull ubuntu</code>。当你不指定版本时,默认拉取latest版本。</p>
<pre><code class="javascript">➜ study-docker git:(master) ✗ docker pull ubuntu
Using default tag: latest
latest: Pulling from library/ubuntu
5b7339215d1d: Pull complete
14ca88e9f672: Pull complete
a31c3b1caad4: Pull complete
b054a26005b7: Pull complete
Digest: sha256:9b1702dcfe32c873a770a32cfd306dd7fc1c4fd134adfb783db68defc8894b3c
Status: Downloaded newer image for ubuntu:latest
➜ study-docker git:(master) ✗ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu latest 4c108a37151f 12 hours ago 64.2MB</code></pre>
<p>也可安装指定版本镜像:<code>docker pull ubuntu:18.04</code></p>
<p>接下来基于<code>ubuntu</code>镜像启动一个容器</p>
<p><code>docker run --name first -it ubuntu bash</code></p>
<ul>
<li>--name 用于指定容器名</li>
<li>it 用于交互式命令行操作,如下面例子运行后,会打开容器的命令行</li>
<li>上面的ubuntu指的镜像,默认基于latest。除非指定版本 如ubuntu:18.04</li>
</ul>
<p>运行上面的命令后,命令行工具就会自动进入容器的命令行。如果想要退出该命令行界面,可输入<code>exit</code>以退出。</p>
<pre><code class="JavaScript">➜ study-docker git:(master) ✗ docker run --name first -it ubuntu bash
root@b7862a018c2c:/# </code></pre>
<p>如果想让该容器在<strong>后台</strong>运行可以通过加<code>-d</code>配置来让该容器在<strong>后台</strong>运行。后台运行,命令行工具不会进入该容器。</p>
<p>使用<code>docker ps</code>查看当前运行中的容器。</p>
<pre><code class="javascript">➜ study-docker git:(master) ✗ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
cf8375f48225 ubuntu "bash" 15 seconds ago Up 14 seconds first</code></pre>
<p>使用<code>-d</code>来让容器在后台运行</p>
<pre><code class="javascript">➜ study-docker git:(master) ✗ docker run --name first -itd ubuntu bash
6df29a09d1f1bb0041b7eb59b5288162471ed8a663007f88c6a30e3fd1f4fbe2</code></pre>
<p>命令行会返回容器id</p>
<p>使用<code>docker container ls</code> 查看所有容器列表(不包括停止运行的容器)</p>
<pre><code class="JavaScript">➜ study-docker git:(master) ✗ docker container ls -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
cf8375f48225 ubuntu "bash" About a minute ago Up About a minute first</code></pre>
<p>使用<code>docker stop <容器id或容器名称></code>停止容器的运行。</p>
<pre><code class="javascript">➜ study-docker git:(master) ✗ docker stop 6df29a09d1f1
6df29a09d1f1</code></pre>
<p>执行命令后,会返回你刚输入容器的id。上面的容器id不需要填全。就想git的commit id一样。</p>
<p>这个时候通过<code>docker container ls</code>是查不到容器信息的。需要用<code>docker container ls -a</code>来查看。</p>
<pre><code class="javascript">➜ study-docker git:(master) ✗ docker container ls -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
6df29a09d1f1 ubuntu "bash" 5 minutes ago Exited (0) 4 minutes ago</code></pre>
<p>可以看到STATUS一栏处,该容器是处于停止状态的。</p>
<p>使用<code>docker rm <容器id 或者 容器昵称></code></p>
<pre><code class="javascript">➜ node git:(master) docker container ls -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
a217eea7188f ubuntu "/bin/bash" 11 seconds ago Exited (0) 10 seconds ago dreamy_ishizaka</code></pre>
<p>执行<code>docker rm a21</code> (id可以不输全)</p>
<pre><code class="javascript">➜ node git:(master) docker rm a21
a21</code></pre>
<p>执行完之后,命令行会返回之前输入的容器id</p>
<p>使用<code>docker container prune</code>,来清空停用状态的容器。</p>
<p>使用<code>docker exec</code>命令进入运行中的容器</p>
<p>如想进入刚才后台运行的容器的交互式界面:<code>docker exec -it <容器名称 或者 容器id> bash</code></p>
<pre><code class="javascript">➜ study-docker git:(master) ✗ docker exec -it first bash
root@2a87b2f62a6e:/#</code></pre>
<p><strong>想查看更多关于docker的命令,<a href="https://link.segmentfault.com/?enc=jDSaFKvhAbNexF4s2Qt%2BWw%3D%3D.AZiTW9wzcQ6YyF8zNN%2B18bGAao1qHlR5eM5hAy2wLzXuG2yez393i5iH6w15JHqckEAtSLXIX1iGRydXzspZdg%3D%3D" rel="nofollow">点击这里</a></strong></p>
<h4>Dockerfile</h4>
<p>举个<code>node</code>镜像的例子</p>
<p>新建一个文件夹</p>
<p>我这里就新建一个名为node的文件夹,具体文件可参照我的github项目的<a href="https://link.segmentfault.com/?enc=KOn4yOB%2BRi7WTWKOG6Aw%2Fw%3D%3D.fAO0Y0Ng1sxftWlJF4Qp6Whv2l3QhwliEi1N8OdOB6z%2BsJHD5BGelsuVyrPJTdyUlDkorjbNcHcsN93SZwyRVw%3D%3D" rel="nofollow">node目录</a></p>
<pre><code class="javascript">// index.js
const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.end('success')
})
app.listen(6001)</code></pre>
<pre><code class="javascript">// Dockerfile
FROM node:8
WORKDIR /home/node
COPY ../ ../
RUN npm install
CMD npm start</code></pre>
<p>指令介绍</p>
<ul>
<li>FROM 我这个node例子是基于node8镜像</li>
<li>WORKDIR 指定工作区。</li>
<li>COPY 将本地目录文件拷贝到docker中</li>
<li>RUN 运行一个容器,每个RUN都会生成一个容器</li>
<li>CMD 执行命令,与RUN相似</li>
</ul>
<blockquote>注意 有必要添加.dockerignore文件,文件中可以填写你不想打包进容器的文件。类似于.gitignore</blockquote>
<pre><code class="javascript">// .dockerignore
/node_modules
package-lock.json</code></pre>
<p>详细解释,详见<a href="https://link.segmentfault.com/?enc=CdcGn24IPbSN0nBIKSLgtg%3D%3D.MQ%2BnrrqJwVLYhbGRN%2BdlmOr9rjsaH3O20gn9R4htSuaZQEoYBc1g37UmESjyeD1%2BNIeW483mv2IUnyMkeR%2B3fOSmibKoS3tL%2FZVjPkabqcQ%3D" rel="nofollow">dockerfile</a></p>
<p><code>docker build</code> 命令用于使用 Dockerfile 创建镜像</p>
<p>执行:<code>docker build -t mynode .</code>;</p>
<ul><li>-t: 镜像的名字及标签,通常 name:tag 或者 name 格式;可以在一次构建中为一个镜像设置多个标签。tag不写默认为latest版本</li></ul>
<p>要注意后面的. 这个表示Dockerfile文件在当前目录。<br>构建镜像成功之后:</p>
<pre><code class="javascript">➜ node git:(master) ✗ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
mynode latest 3cd10521f802 10 hours ago 898MB</code></pre>
<p>接下来就基于该镜像运行一个node容器:</p>
<p><code>docker run --name mynode -p 4001:6001 mynode</code></p>
<ul>
<li>--name: 表示该容器的匿名</li>
<li>-p: 表示端口映射,因为主机的ip跟容器的ip是不同的,需要把容器的服务映射到0.0.0.0:自己设置的主机端口,host不填默认为0.0.0.0。 <主机端口>:<容器中端口>;</li>
</ul>
<pre><code class="javascript">➜ node git:(master) ✗ docker run --name mynode -p 4001:6001 mynode
> example2@1.0.0 start /home/node
> node index.js</code></pre>
<p>浏览器访问<code>localhost:4001</code>,页面会展示出node响应的<code>success</code>字符串了。</p>
<p>构建完镜像后,你觉得不需要该镜像,想删除怎么办呢?</p>
<p>首先执行:<code>docker images</code>列出镜像列表</p>
<pre><code class="javascript">➜ node git:(master) ✗ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
mynode latest 3cd10521f802 10 hours ago 898MB</code></pre>
<p>使用<code>docker rmi <image id></code>来删除镜像:<code>docker rmi 3cd10521f802</code>,如果提醒该镜像被容器占用着,那么你就需要先删除该容器(参考上面介绍的命令)。</p>
webpack4学习笔记(四)
https://segmentfault.com/a/1190000019182490
2019-05-15T01:44:36+08:00
2019-05-15T01:44:36+08:00
我是南方大汉
https://segmentfault.com/u/woshinanfangdahan
1
<h2>前言</h2>
<p>这是我花了几个星期学习webpack4的学习笔记。内容不够细,因为一些相对比较简单的,就随意带过了。希望文章能给大家带来帮助。如有错误,希望及时指出。例子都在<a href="https://link.segmentfault.com/?enc=%2B7dgC4YNdWS6Ug1jXcnjNA%3D%3D.NXCWycZxXp1EcwwfAagAxFio70Voe%2BtvPBCCRXNUE0nHvCg%2BPClVVW0a5R9nO7S3" rel="nofollow">learn-webpack</a>仓库上。如果你从中有所收获的话,希望你能给我的<code>github</code>点个<code>star</code>。</p>
<h3>编写loader</h3>
<pre><code class="javascript">// index.js
console.log('hello, atie')</code></pre>
<p>配置<code>webpack.config.js</code></p>
<pre><code class="javascript">// webpack.config.js
module: {
rules: [
{
test: /\.js$/,
include: /src/,
loader: path.resolve(__dirname, './loaders/replaceLoader.js')
}
]
},</code></pre>
<pre><code class="javascript">// 函数不能使用箭头函数
module.exports = function(source) {
console.log(source, 'source')
return source.replace('atie', 'world')
}</code></pre>
<p><code>loader</code>文件其实就是导出一个函数,<code>source</code>就是<code>webpack</code>打包出的<code>js</code>字符串。这里的<code>loader</code>就是将上面的<code>console.log('hello, atie')</code>替换为<code>console.log('hello, world')</code></p>
<p>打包下代码,不出所料。控制台就会打印出<code>hello, world</code></p>
<p>当你想要给loader传参时,可配置如下</p>
<pre><code class="javascript">module: {
rules: [
{
test: /\.js$/,
include: /src/,
use: [{
loader: path.resolve(__dirname, './loaders/replaceLoader.js'),
options: {
name: 'haha'
}
}]
}
]
},</code></pre>
<p>通过给<code>loader</code>添加<code>options</code></p>
<p>这样<code>loader</code>中就可以通过<code>this.query</code>获取该参数了</p>
<pre><code class="javascript">module.exports = function(source) {
// 控制台输出:console.log('hello atie') { name: 'haha' } source
console.log(source, this.query, 'source')
return source.replace('atie', 'world')
}</code></pre>
<p>当然变量不一定非要通过<code>this.query</code>来获取</p>
<p>可通过<code>loader-utils</code>这个包来获取传入的变量</p>
<p>安装: <code>npm i loader-utils -D</code></p>
<pre><code class="javascript">const loaderUtils = require('loader-utils')
// 函数不能使用箭头函数
module.exports = function(source) {
// console.log(source, this.query, 'source')
const options = loaderUtils.getOptions(this)
console.log(options, 'options') // { name: 'haha' } 'options'
return source.replace('atie', 'world')
}</code></pre>
<p>打印出来的与上面<code>this.query</code>一致</p>
<p>上面都是直接通过<code>return</code>返回的,那么我们还有没有其他方法返回<code>loader</code>翻译后的代码呢?`</p>
<p>这里就会用到<code>callback</code></p>
<pre><code class="javascript">this.callback(
err: Error | null,
content: string | Buffer,
sourceMap?: SourceMap,
meta?: any
);</code></pre>
<p>上面的代码就可以改写成</p>
<pre><code class="javascript">module.exports = function(source) {
const options = loaderUtils.getOptions(this)
const result = source.replace('atie', options.name)
this.callback(null, result)
}</code></pre>
<p><code>callback</code>优势在于它可以传递多余的参数</p>
<pre><code class="javascript">module.exports = function(source) {
setTimeout(() => {
return source.replace('atie', 'world')
}, 1000)
}</code></pre>
<p>当我们把<code>return</code>包到异步方法里,打包的时候就会报错,那么我们该怎么办呢?</p>
<p>这个时候就需要用到<code>this.async()</code></p>
<pre><code class="javascript">module.exports = function(source) {
const callback = this.async()
setTimeout(() => {
callback(null, source.replace('atie', 'world'))
}, 2000)
}</code></pre>
<p>通过调用<code>this.async()</code>返回的<code>callback</code>方法来返回结果</p>
<p><strong>use中的loader执行顺序,先右后左,先下后上</strong></p>
<h3>编写plugin</h3>
<p>在根目录下新建<code>plugins</code>文件夹,并新建<code>copyright-webpack-plugin.js</code>,内容如下:</p>
<pre><code class="javascript">class Copyright {
constructor() {
console.log('this is plugin')
}
apply(compiler) {
}
}
module.exports = Copyright</code></pre>
<p>注意:apply这个方法必须存在,不然插件被执行的时候会报错。</p>
<p>配置<code>webpack.config.js</code>,如下:</p>
<pre><code class="javascript">const Copyrgiht = require('./plugins/copyright-webpack-plugin.js')
...
plugins: [
new Copyrgiht()
]</code></pre>
<p>执行下打包命令后</p>
<pre><code class="javascript">this is plugin
Hash: 479baeba2207182096f8
Version: webpack 4.30.0
Time: 615ms
Built at: 2019-05-08 23:05:08
Asset Size Chunks Chunk Names
bundle.js 3.77 KiB main [emitted] main
index.html 182 bytes [emitted] </code></pre>
<p>控制台打印出了<code>this is plugin</code></p>
<p>接下来,我们继续探索插件的奥秘</p>
<p>在使用插件的时候还可以传参</p>
<pre><code class="javascript">// webpack.config.js
plugins: [
new Copyrgiht({
name: 'atie'
})
]</code></pre>
<pre><code class="javascript">class Copyright {
constructor(options) {
// console.log(options, 'this is plugin')
this.options = options
}
apply(compiler) {
console.log(this.options)
}
}</code></pre>
<p>执行下打包命令:</p>
<pre><code class="javascript">{ name: 'atie' }
Hash: 479baeba2207182096f8
Version: webpack 4.30.0
Time: 742ms
Built at: 2019-05-08 23:24:10
Asset Size Chunks Chunk Names
bundle.js 3.77 KiB main [emitted] main
index.html 182 bytes [emitted]</code></pre>
<p>控制就会输出 <code>{name: 'atie'}</code></p>
<p><code>webpack</code>在调用<code>apply</code>会传递一个<code>compiler</code>参数,这个参数可以做很多事情,具体可以参考<code>webpack</code>官网</p>
<p>这里介绍下钩子</p>
<pre><code class="javascript">class Copyright {
apply(compiler) {
compiler.hooks.emit.tapAsync('Copyright', (compilation,callback) => {
console.log(compilation.assets, '以具有延迟的异步方式触及 run 钩子。');
compilation.assets['copyright.txt'] = {
source: function() {
return 'copyright by atie'
},
size: function() {
return 17
}
}
callback()
})
}
}
module.exports = Copyright</code></pre>
<p>该钩子是在文件生成前触发的。我们在文件生成前,在<code>asset</code>对象上在加一个文件对象</p>
<p>打包之后</p>
<pre><code class="javascript">.
├── bundle.js
├── copyright.txt
└── index.html</code></pre>
<p>可以看到多了一个<code>copyright.txt</code>,也就是我们上面创建的文件。点开该文件还会看到里面的内容正是<code>copyright by atie</code></p>
webpack4学习笔记(三)
https://segmentfault.com/a/1190000019182481
2019-05-15T01:41:45+08:00
2019-05-15T01:41:45+08:00
我是南方大汉
https://segmentfault.com/u/woshinanfangdahan
1
<h2>前言</h2>
<p>这是我花了几个星期学习webpack4的学习笔记。内容不够细,因为一些相对比较简单的,就随意带过了。希望文章能给大家带来帮助。如有错误,希望及时指出。例子都在<a href="https://link.segmentfault.com/?enc=NfLB6rnH0C3IMXgaRV7pJw%3D%3D.KVp75rQC77uarh22D0Tdoh3GVCXz3rKzQKNrTNESWMYl1KLqP7HwSxGrIm6Y3%2B0V" rel="nofollow">learn-webpack</a>仓库上。如果你从中有所收获的话,希望你能给我的<code>github</code>点个<code>star</code>。</p>
<h3>library</h3>
<p>当你要开发第三方库供别人使用时,就需要用到<code>library</code>和<code>libraryTarget</code>这两个配置了。</p>
<p><strong><code>library</code></strong></p>
<pre><code class="javascript">output: {
filename: 'library.js',
library: 'library',
libraryTarget: 'umd'
},</code></pre>
<p><code>library</code>: 当配置了这个<code>library</code>参数后,会把<code>library</code>这个<code>key</code>对应的<code>value</code>即上面代码<code>library</code>挂载到全局作用域中。<code>html</code>用<code>script</code>标签引入,可以通过访问全局变量<code>library</code>访问到我们自己开发的库。</p>
<p><code>libraryTarget</code>:这个默认值为<code>var</code>。意思就是让library定义的变量挂载到全局作用域下。当然还有浏览器环境的<code>window</code>,<code>node</code>环境的<code>global</code>,<code>umd</code>等。当设置了<code>window</code>、<code>global</code>,<code>library</code>就会挂载到这两个对象上。当配置了<code>umd</code>后,你就可以通过<code>import</code>,<code>require</code>等方式引入了。</p>
<p><strong><code>externals</code></strong></p>
<p><code>exterals</code>是开发公共库很重要的一个配置。当你的公共库引入了第三方库的时候,公共库会把该第三方库也打包进你的模块里。当使用者也引入了这个第三方库后,这个时候打包就会又打了一份第三方库进来。</p>
<p>所在在公共模块库中配置如下代码</p>
<pre><code class="javascript">externals: {
// 前面的lodash是我的库里引入的包名 比如 import _ from 'lodash'
// 后面的lodash是别人业务代码需要注入到他自己模块的lodash 比如 import lodash from 'lodash',注意不能import _ from 'lodash',因为配置项写了lodash 就不能import _
lodash: 'lodash'
},</code></pre>
<p><strong>前面的<code>lodash</code>是我的库里引入的包名 比如<code> import _ from 'lodash'</code>,后面的<code>lodash</code>是别人业务代码需要注入到他自己模块的<code>lodash</code> 比如 <code>import lodash from 'lodash'</code>,注意不能<code>import _ from 'lodash'</code>,因为配置项写了<code>lodash</code> 就不能<code>import _</code>。</strong></p>
<p>本人做了个试验,当自己开发的包不引入<code>lodash</code>,业务代码中也不引入<code>lodash</code>,那么打包业务代码的时候,<code>webpack</code>会把<code>lodash</code>打进你业务代码包里。</p>
<p>当然<code>externals</code>,配置还有多种写法,如下</p>
<pre><code class="javascript">externals: {
lodash: {
commonjs: 'lodash',
commonjs2: 'lodash',
amd: 'lodash',
root: '_'
}
}
externals: ['lodash', 'jquery']
externals: 'lodash'</code></pre>
<p>具体请参考官网<a>externals</a></p>
<h3>发布自己开发的npm包</h3>
<p>学了上面的配置后,就需要学习下如何将自己的包发布到<code>npm</code>仓库上了。</p>
<ul>
<li>
<code>package.json</code> 的入口要改成<code>dist</code>目录下的js文件如: <code>"main": "./dist/library.js"</code>
</li>
<li>注册npm账号。npm会发送一份邮件到你的邮箱上,点击下里面的链接进行激活。</li>
<li>命令行输入<code>npm login</code> 进行登录,或者<code>npm adduser</code> 添加账号</li>
<li><code>npm publish</code></li>
</ul>
<p>当出现如下提示代表发布成功</p>
<pre><code class="javascript">// 当出现类似如下代码时,表示你已经发布成功
➜ library git:(master) ✗ npm publish
+ atie-first-module-library@1.0.0</code></pre>
<p>遇到的问题:</p>
<p>当你遇到<code>npm ERR! you must verify your email before publishing a new package</code>说明你还没有激活你的邮箱,去邮箱里点击下链接激活下就ok了</p>
<p>当你已经登录了提醒<code>npm ERR! 404 unauthorized Login first</code>,这个时候你就要注意下你的<code>npm</code>源了,看看是否设置了淘宝源等。记得设置回来<code>npm config set registry https://registry.npmjs.org/</code></p>
<h3>PWA</h3>
<p>http-server</p>
<p>workbox-webpack-plugin</p>
<p>相信很多朋友都有耳闻过<code>PWA</code>这门技术,<code>PWA</code>是<code>Progressive Web App</code>的英文缩写, 翻译过来就是渐进式增强WEB应用, 是Google 在2016年提出的概念,2017年落地的web技术。目的就是在移动端利用提供的标准化框架,在网页应用中实现和原生应用相近的用户体验的渐进式网页应用。</p>
<p>优点:</p>
<ol>
<li>
<strong>可靠</strong> 即使在不稳定的网络环境下,也能瞬间加载并展现</li>
<li>
<strong>快</strong> 快速响应,并且 动画平滑流畅</li>
</ol>
<p>应用场景:</p>
<p>当你访问正常运行的服务器页面的时候,页面正常加载。可当你服务器挂了的时候,页面就无法正常加载了。</p>
<p>这个时候就需要使用到pwa技术了。</p>
<p>这里我编写最简单的代码重现下场景:</p>
<pre><code class="javascript">// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CleanWebpackPlugin = require('clean-webpack-plugin')
module.exports = {
mode: 'production',
entry: './index.js',
output: {
filename: 'bundle.js'
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin()
]
}
// index.js
console.log('this is outer console')
// package.json
{
"name": "PWA",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "webpack --config webpack.config.js",
"start": "http-server ./dist"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"clean-webpack-plugin": "^2.0.1",
"html-webpack-plugin": "^3.2.0",
"http-server": "^0.11.1",
"webpack": "^4.30.0",
"webpack-cli": "^3.3.1",
}
}
</code></pre>
<p>执行下<code>npm run build</code></p>
<pre><code class="javascript">.
├── bundle.js
└── index.html</code></pre>
<p>为了模拟服务器环境,我们安装下<code>http-server</code></p>
<p><code>npm i http-server -D</code></p>
<p>配置下<code>package.json</code>,<code>"start": "http-server ./dist"</code></p>
<p>执行<code>npm run start</code>来启动dist文件夹下的页面</p>
<p>这个时候控制台会正常打印出<code>'this is outer console'</code></p>
<p>当我们断开<code>http-server</code>服务后,在访问该页面时,页面就报404了</p>
<p>这个时候就需要使用到pwa技术了</p>
<p>使用步骤:</p>
<p>安装: <code>npm i workbox-webpack-plugin -D</code></p>
<p>webpack配置文件配置:</p>
<pre><code class="javascript">// webpack.config.js
const {GenerateSW} = require('workbox-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CleanWebpackPlugin = require('clean-webpack-plugin')
module.exports = {
mode: 'production',
entry: './index.js',
output: {
filename: 'bundle.js'
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin(),
new GenerateSW({
skipWaiting: true, // 强制等待中的 Service Worker 被激活
clientsClaim: true // Service Worker 被激活后使其立即获得页面控制权
})
]
}</code></pre>
<p>这里我们写一个最简单的业务代码,在注册完pwa之后打印下内容:</p>
<pre><code class="javascript">// index.js
console.log('this is outer console')
// 进行 service-wroker 注册
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker
.register('./service-worker.js')
.then(registration => {
console.log('====== this is inner console ======')
console.log('SW registered: ', registration);
})
.catch(registrationError => {
console.log('SW registration failed: ', registrationError);
});
});
}</code></pre>
<p>执行下打包命令:<code>npm run build</code></p>
<pre><code class="javascript">.
├── bundle.js
├── index.html
├── precache-manifest.e21ef01e9492a8310f54438fcd8b1aad.js
└── service-worker.js</code></pre>
<p>打包之后会生成个<code>service-worker.js</code>与<code>precache-manifest.e21ef01e9492a8310f54438fcd8b1aad.js</code></p>
<p>接下来再重启下<code>http-server</code>服务:<code>npm run start</code></p>
<p>页面将会打印出</p>
<pre><code class="javascript">this is outer console
====== this is inner console ======
...</code></pre>
<p>然后我们再断开<code>http-server</code>服务</p>
<p>刷新下页面,竟然打印出了相同的代码。说明pwa离线缓存成功。</p>
<h3>typescript</h3>
<p>使用webpack打包ts文件,就需要安装<code>ts-loader</code></p>
<p>安装:<code>npm i ts-loader typescript -D</code></p>
<p><code>webpack.config.js</code>文件中添加解析<code>typescript</code>代码的<code>loader</code></p>
<pre><code class="javascript">const HtmlWebpackPlugin = require('html-webpack-plugin')
const CleanWebpackPlugin = require('clean-webpack-plugin')
module.exports = {
mode: 'production',
entry: './src/index.ts',
output: {
filename: 'bundle.js'
},
module: {
rules: [
{
test: /\.ts$/,
loader: 'ts-loader',
exclude: /node_modules/
}
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin()
]
}</code></pre>
<p>配置了<code>webpack.config.js</code>还不行,还得在根目录文件下新增个<code>.tsconfig.json</code>文件</p>
<pre><code class="javascript">{
"compilerOptions": {
"outDir": "./dist/", // 默认解析后的文件输出位置
"noImplicitAny": true, // 存在隐式 any 时抛错
"module": "es6", // 表示这是一个es6模块机制
"target": "es5", // 表示要讲ts代码转成es5代码
"allowJs": true // 表示允许引入js文件。TS 文件指拓展名为 .ts、.tsx 或 .d.ts 的文件。如果开启了 allowJs 选项,那 .js 和 .jsx 文件也属于 TS 文件
}
}</code></pre>
<p>新建<code>index.ts</code></p>
<pre><code class="javascript">class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
let greeter = new Greeter("world");
let button = document.createElement('button');
button.textContent = "Say Hello";
button.onclick = function() {
alert(greeter.greet());
}
document.body.appendChild(button);</code></pre>
<p>执行打包命令,访问打包后的页面,页面正常执行。</p>
<p>当需要使用<code>lodash</code>等库时,</p>
<p>需安装:<code>npm i @types/lodash -D</code></p>
<p>修改页面代码 引入 <code>lodash</code></p>
<pre><code class="javascript">import * as _ from 'lodash'
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
let greeter = new Greeter("world");
let button = document.createElement('button');
button.textContent = "Say Hello";
button.onclick = function() {
alert(_.join(['lodash', greeter.greet()], '-'));
}
document.body.appendChild(button);</code></pre>
<p><strong>提醒:ts使用的包,可通过<code>https://microsoft.github.io/TypeSearch</code> 这个网址去查对应的包使用指南</strong></p>
<h3>使用<code>WebpackDevServer</code>实现请求转发</h3>
<p>当我们工作本地开发某一个需求的时候,需要将这块需求的请求地址转发到某个后端同事的本地服务器或某个服务器上,就需要用到代理。然后其他页面的请求都走测试环境的请求。那么我们该怎样拦截某个请求,并将其转发到我们想要转发的接口上呢?</p>
<p>这个时候就需要用到<code>webpack-dev-server</code></p>
<p>主要看<code>devServer</code>配置:</p>
<pre><code class="javascript">// webpack.config.js
const CleanWebpackPlugin = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode: "development",
entry: './index.js',
output: {
filename: 'bundle.js'
},
devServer: {
contentBase: './dist',
open: true,
hot: true
},
plugins: [
new HtmlWebpackPlugin(),
new CleanWebpackPlugin()
]
}</code></pre>
<pre><code class="javascript">// package.json
scripts: {
"server": "webpack-dev-server"
}</code></pre>
<pre><code class="javascript">// index.js
import axios from 'axios'
const div = document.createElement('div')
div.innerHTML = 'hahahha'
div.addEventListener('click', () => {
alert('hahah')
axios.get('/list').then(res => {
console.log(res)
})
})
document.body.appendChild(div)</code></pre>
<p>在写一个本地启动的服务端代码</p>
<pre><code class="javascript">const express = require('express')
const app = express()
app.get('/api/list', (req, res) => {
res.json({
success: true
})
})
app.listen(8888, () => {
console.log('listening localhost:8888')
})</code></pre>
<p>执行<code>npm run server</code>命令,浏览器会自动打开页面。点击div后,会发起请求。</p>
<p>浏览器提示<code>http://localhost:8080/api/list 404 (Not Found)</code>,表示该接口不存在。</p>
<p>因为我们<code>webpack</code>启动静态资源服务器默认端口为8080,所以他求会直接请求到8080的/api/list接口。所以会提示找不到该接口。</p>
<p>为了解决这个问题,我们就需要将该请求从8080端口代理到8888端口(也就是我们自己本地启动的服务)</p>
<p>配置<code>webpack.config.js</code></p>
<p>这里我只展示<code>devServer</code>代码</p>
<pre><code class="javascript">// webpack.config.js
devServer: {
contentBase: './dist',
open: true,
hot: true,
proxy: {
'/api': {
target: 'http://localhost:8888'
}
}
},</code></pre>
<p>配置<code>devServer</code>的<code>proxy</code>字段的参数,将请求<code>/api</code>开头的请求都转发到<code>http://localhost:8888</code>,</p>
<p>通过这个方法可以解决一开始提到的本地开发的时候,只想把部分接口转发到某台部署新需求的服务器上。比如当你这个项目请求很多,不同接口部署在不同的端口或者不同的服务器上。那么就可以通过配置<strong>第一个路径</strong>,来区分不同的模块。并转发到不同的服务上。如:</p>
<pre><code class="javascript">// webpack.config.js
devServer: {
contentBase: './dist',
open: true,
hot: true,
proxy: {
'/module1': {
target: 'http://localhost:8887'
},
'/module2': {
target: 'http://localhost:8888'
},
'/module3': {
target: 'http://localhost:8889'
}
}
},</code></pre>
<p>当你要代理到某个https的接口上,就需要设置<code>secure: false</code></p>
<pre><code class="javascript">// webpack.config.js
devServer: {
proxy: {
'/api': {
target: 'https://other-server.example.com',
secure: false
}
}
}</code></pre>
<pre><code class="javascript">target: '', // 代理的目标地址
secure: false, // 请求https的需要设置
changeOrigin: true, // 跨域的时候需要设置
headers: {
host: 'http://www.baidu.com', //修改请求域名
cookie: ''
}
...</code></pre>
<p>其他关于<code>devServer</code>的配置详见<a>devServer</a></p>
<h3>WebpackDevServer解决单页面路由404问题</h3>
<p>相信大家都是开发过vue或者react单页面带路由的应用。这里就忽略业务代码,介绍下<code>devServer</code>的<code>historyApiFallback</code>参数</p>
<pre><code class="javascript">devServer: {
historyApiFallback: true, // 当设置为true时,切换路由就不会出现路由404的问题了
}</code></pre>
<p>详见<a>historyApiFallback</a></p>
<h3>eslint</h3>
<p>安装<code>eslint</code>: <code>npm i eslint -D</code></p>
<p>目录下新建<code>.eslintrc.json</code>文件。</p>
<p><code>environment</code>: 指定脚本的运行环境</p>
<p><code>globals</code>: 脚本在执行期间访问的额外全局变量。</p>
<p><code>rules</code>: 启动的规则及其各自的错误级别。</p>
<p><code>解析器选项</code>: 解析器选项</p>
<p>编辑你的<code>eslint</code>的规则</p>
<pre><code class="javascript">{
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
},
"rules": {
"semi": 2
}
}</code></pre>
<p><code>vscode</code>安装<code>eslint</code>插件。</p>
<p>配置下<code>webpack.config.js</code>配置。</p>
<pre><code class="javascript">...
devServer: {
overlay: true,
contentBase: './dist',
hot: true,
open: true
},
module: {
rules: [{
test: /\.js$/,
exclude: /node_modules/,
use: ['eslint-loader']
}]
}
...</code></pre>
<p><code>eslint-loader</code>是用于检查<code>js</code>代码是否符合<code>eslint</code>规则。</p>
<p>这里<code>devServer</code>中的<code>overlay</code>的作用是,当你eslint报错的时候,页面会有个报错蒙层。这样就不局限于编辑器(vscode)的报错提醒了。</p>
<p>如果js代码使用了多个loader,那么eslint-loader一定要写在最右边。如果不写在最后一个的话,需在里面添加<code>enforce: "pre"</code>,这样不管写在哪个位置都会优先使用<code>eslint-loader</code>校验下代码规范。</p>
<pre><code class="javascript">{
loader: 'eslint-loader',
options: {
enforce: "pre",
}
}</code></pre>
<h3>提升<code>webpack</code>打包速度的方法</h3>
<h4>1. 跟上技术的迭代</h4>
<ul><li>升级<code>webpack</code>版本 <code>node</code>版本<code> npm</code>等版本</li></ul>
<h4>2. 尽可能少的模块上应用<code>loader</code>
</h4>
<ul><li>
<code>include</code> <code>exclude</code>
</li></ul>
<h4>3. 尽可能少的使用<code>plugin</code>
</h4>
<h4>4. <code>resolve</code>
</h4>
<pre><code class="javascript">resolve: {
extensions: ['.js'],
alias: {
'src': path.resolve(__dirname, '../src')
}
}</code></pre>
<p><code>extensions</code>: 可以让你import模块的时候不写格式,当你不写格式的时候,webpack会默认通过extensions中的格式去相应的文件夹中找</p>
<p><code>alias</code>:当你<code>import</code>的路径很长的时候,最好使用别名,能简化你的路径。</p>
<p>比如:<code>import index.js from '../src/a/b/c/index.js'</code></p>
<p>设置别名:</p>
<pre><code class="javascript">resolve: {
alias: {
'@c': path.resolve(__dirname, '../src/a/b/c')
}
}</code></pre>
<p>这样你的<code>import</code>导入代码就可以改成<code>import index.js from '@c/index.js'</code></p>
<h4>5. dllPlugin</h4>
<p>我们先记录下不使用<code>dll</code>打包时间<code>787ms</code>:</p>
<pre><code class="javascript">Time: 787ms
Built at: 2019-05-04 18:32:29
Asset Size Chunks Chunk Names
bundle.js 861 KiB main [emitted] main
index.html 396 bytes [emitted] </code></pre>
<p>接下来我们就尝试使用<code>dll</code>技术</p>
<p>我们先配置一个用于打包<code>dll</code>文件的<code>webpack</code>配置文件,生成打包后的<code>js</code>文件与描述动态链接库的<code>manifest.json</code></p>
<pre><code class="javascript">// webpack.dll.config.js
const path = require('path')
const webpack = require('webpack')
module.exports = {
entry: {
vendor: ['jquery', 'lodash'] // 要打包进vendor的第三方库
},
output: {
filename: '[name].dll.js', // 打包后的文件名
path: path.resolve(__dirname, './dll'), // 打包后存储的位置
library: '[name]_[hash]' // 挂载到全局变量的变量名,这里要注意 这里的library一定要与DllPlugin中的name一致
},
plugins: [
new webpack.DllPlugin({ // 用于打包出一个个单独的动态链接库文件
name: '[name]_[hash]', // 引用output打包出的模块变量名,切记这里必须与output.library一致
path: path.join(__dirname, './dll', '[name].manifest.json') // 描述动态链接库的 manifest.json 文件输出时的文件名称
})
]
}</code></pre>
<p><strong>重点:这里引入的DllPlugin插件,该插件将生成一个manifest.json文件,该文件供webpack.config.js中加入的DllReferencePlugin使用,使我们所编写的源文件能正确地访问到我们所需要的静态资源(运行时依赖包)。</strong></p>
<p>配置下<code>package.json</code>文件的<code>scripts</code>: <code>"build:dll": "webpack --config webpack.dll.config.js"</code></p>
<p>执行下 <code>npm run build:dll</code></p>
<pre><code class="javascript">Time: 548ms
Built at: 2019-05-04 18:54:09
Asset Size Chunks Chunk Names
vendor.dll.js 157 KiB 0 [emitted] vendor</code></pre>
<p>除了打包出<code>dll</code>文件之外,还得再主<code>webpack</code>配置文件中引入。这里就需要使用到<code>DllReferencePlugin</code>。具体配置如下:</p>
<pre><code class="javascript">// webpack.config.js
new webpack.DllReferencePlugin({
manifest: require('./dll/vendor.manifest.json')
}),</code></pre>
<p>这里的<code>manifest</code>:需要配置的是你<code>dllPlugin</code>打包出来的<code>manifest.json</code>文件。让主<code>webpack</code>配置文件通过这个</p>
<p>描述动态链接库<code>manifest.json</code>文件,让<code>js</code>导入该模块的时候,直接引用<code>dll</code>文件夹中打包好的模块。</p>
<p>看似都配置好了,接下来执行下命令 <code>npm run build</code></p>
<p>使用<code>dll</code>打包后时间:</p>
<pre><code class="javascript">Time: 471ms
Built at: 2019-05-04 18:19:49
Asset Size Chunks Chunk Names
bundle.js 6.43 KiB main [emitted] main
index.html 182 bytes [emitted] </code></pre>
<p><strong>直接从最开始的<code>787ms</code>降低到<code>471ms</code>,当你抽离的第三方模块越多,这个效果就越明显。</strong></p>
<p>浏览器跑下<code>html</code>页面,会报错</p>
<p><code>Uncaught ReferenceError: vendor_e406fbc5b0a0acb4f4e9 is not defined</code></p>
<p>这是因为<code>index.html</code>还需要通过<code>script</code>标签引入这个<code>dll</code>打包出来的<code>js</code>文件</p>
<p>我们如果每次自己手动引入的话会比较麻烦,如果<code>dll</code>文件非常多的话,就难以想象了。</p>
<p>这个时候就需要借助<code>add-asset-html-webpack-plugin</code>这个包了。</p>
<pre><code class="javascript">const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin')
new AddAssetHtmlPlugin({
filepath: path.resolve(__dirname, './dll/vendor.dll.js')
})</code></pre>
<p>通过这包,<code>webpack</code>会将<code>dll</code>打包出来的<code>js</code>文件通过<code>script</code>标签引入到<code>index.html</code>文件中</p>
<p>这个时候你在<code>npm run build</code>,访问下页面就正常了</p>
<h4>6. 控制包文件大小</h4>
<p>tree-shaking 等</p>
<h4>7. thread-loader parallel-webpack happypack等多进程打包</h4>
<h4>8. 合理使用sourceMap</h4>
<h4>9. 结合stats分析打包结果</h4>
<p>借助线上或者本地打包分析工具</p>
<h4>10. 开发环境内存编译</h4>
<p>开发环境的时候不会生成dist文件夹,会直接从内存中读取,因为内存读取比硬盘读取快</p>
<h4>11. 开发环境无用插件剔除</h4>
webpack4学习笔记(二)
https://segmentfault.com/a/1190000019182473
2019-05-15T01:35:27+08:00
2019-05-15T01:35:27+08:00
我是南方大汉
https://segmentfault.com/u/woshinanfangdahan
0
<h2>前言</h2>
<p>这是我花了几个星期学习webpack4的学习笔记。内容不够细,因为一些相对比较简单的,就随意带过了。希望文章能给大家带来帮助。如有错误,希望及时指出。例子都在<a href="https://link.segmentfault.com/?enc=nHYI4WhgyOylQRc74zNu5g%3D%3D.cPB7BijPFIAfX6rw2GGuDrTLTLLIXhFQeE%2BRdbB4s6JF34LOjcN0aFlI2YuRu1hW" rel="nofollow">learn-webpack</a>仓库上。如果你从中有所收获的话,希望你能给我的<code>github</code>点个<code>star</code>。</p>
<h3>tree shaking</h3>
<p>一个模块里会导出很多东西。把一个模块里没有被用到的东西都给去掉。不会把他打包到入口文件里。tree shaking只支持es6的方式引入(<code>import</code>),使用<code>require</code>无法使用<code>tree shaking</code>。</p>
<p><code>webpack</code>的<code>development</code>无法使用<code>tree shaking</code>功能。除非在打包的配置里加上</p>
<pre><code class="javascript">// 开发环境需要加如下代码
optimization: {
usedExports: true
}</code></pre>
<p>当你需要import某个模块,又不想<code>tree shaking</code>把他给干掉,就需要在package.json里修改<code>sideEffects</code>参数。比如当你<code>import './console.js' </code>, <code>import './index.css'</code>等没有<code>export</code>(导出)模块的文件。又不想<code>tree shaking</code>把它干掉。</p>
<pre><code class="javascript">// package.json
sideEffects: ['./console.js', './index.css']
// 反之
sideEffects: false</code></pre>
<p><strong>在<code>development</code>环境即使你使用<code>tree shaking</code>,它也不会把其他多余的代码给干掉。他只会在打包的文件里注明某段代码是不被使用的。</strong></p>
<h3>
<code>development</code> 和 <code>production</code> 区别</h3>
<p><code>development</code>代码不压缩,<code>production</code>代码会压缩</p>
<p>省略…☺</p>
<p><code>webpack-merge</code></p>
<p><code>react</code>和<code>vue</code>都会区分环境进行不同的<code>webpack</code>配置,但是它们一定会有相同的部分。这个时候需要通过使用<code>webpack-merge</code>进行抽离。</p>
<pre><code class="javascript">// webpack.base.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode: 'production',
// mode: 'development',
entry: './index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, './dist')
},
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
},
optimization: {
usedExports: true
},
plugins: [
new HtmlWebpackPlugin({
template: './index.html'
})
]
}
// webpack.dev.config.js
const merge = require('webpack-merge')
const baseConfig = require('./webpack.base.config')
const devConfig = {
mode: 'development',
}
module.exports = merge(baseConfig, devConfig)</code></pre>
<p>这里就不重复把<code>production</code>环境在配置出来了,主要介绍下<code>webpack-merge</code>用法。</p>
<ul>
<li>安装<code>npm i webpack-merge -D</code>
</li>
<li>新建一个公共的文件如:<code>webpack.base.config.js</code>
</li>
<li>将<code>development</code>和<code>production</code>两个<code>webpack</code>配置相同的抽离到<code>webpack.base.config.js</code>文件中</li>
<li>
<p>在环境配置文件中(具体代码如上)</p>
<ul>
<li><code>const merge = require('webpack-merge')</code></li>
<li><code>const baseConfig = require('./webpack.base.config.js')</code></li>
<li><code>module.exports = merge(baseConfig, devConfig)</code></li>
</ul>
</li>
</ul>
<h3>
<code>code splitting</code>和<code>splitChunks</code>
</h3>
<p>当你把所有的代码都打包到一个文件的时候,每次改一个代码都需要重新打包。且用户都要重新加载下这个js文件。但是如果你把一些公共的代码或第三方库抽离并单独打包。通过缓存加载,会加快页面的加载速度。</p>
<ol>
<li>异步加载的代码,webpack会单独打包到一个js文件中</li>
<li>同步加载的代码有两种方式</li>
</ol>
<p>原始代码</p>
<pre><code class="javascript">import _ from 'lodash'
console.log(666)</code></pre>
<p>打包后的文件:</p>
<p><code>main.js 551 KiB main [emitted] main</code><br>可以看到,webpack将业务代码跟lodash库打包到一个main.js文件了</p>
<p>方法一:</p>
<p>创建一个新文件</p>
<pre><code class="javascript">import _ from 'lodash'
window._ = _</code></pre>
<p>将文件挂载到<code>window</code>对象上,这样其他地方就可以直接使用了。</p>
<p>然后在webpack配置文件中的entry增加一个入口为该文件。让该文件单独打包。</p>
<pre><code class="javascript"> Asset Size Chunks Chunk Names
lodash.js 551 KiB lodash [emitted] lodash
main.js 3.79 KiB main [emitted] main</code></pre>
<p>方法二:</p>
<p>通过添加<code>optimization</code>配置参数</p>
<p><code>optimization</code>: 会将诸如<code>lodash</code>等库抽离成单独的<code>chunk</code>,还会将多个模块公用的模块抽离成单独的<code>chunk</code></p>
<pre><code class="javascript">optimization: {
splitChunks: {
chunks: 'all'
}
},</code></pre>
<p>打包后文件:</p>
<pre><code class="javascript"> Asset Size Chunks Chunk Names
main.js 6.78 KiB main [emitted] main
vendors~main.js 547 KiB vendors~main [emitted] vendors~main</code></pre>
<p>可以看到,webpack将lodash抽成公共的chunk打包出来了。</p>
<p><code>splitChunks</code>里面还可以在添加个参数<code>cacheGroups</code></p>
<pre><code class="javascript">optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
// 下面的意思是:将从node_modules中引入的模块统一打包到一个vendors.js文件中
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
filename: 'vendors.js'
},
default: false
}
}
}</code></pre>
<p><code>cacheGroups</code>中<code>vendors</code>配置表示将从<code>node_modules</code>中引入的模块统一打包到一个vendors.js文件中</p>
<p><code>splitChunks</code>的<code>vendors</code>的<code>default</code>参数:</p>
<p>根据上下文来解释,如上配置了<code>vendors</code>,打包<code>node_modules</code>文件夹中的模块,</p>
<p>那么<code>default</code>将会打包自己编写的公共方法。</p>
<p>当不使用<code>default</code>配置时。</p>
<pre><code class="javascript"> Asset Size Chunks Chunk Names
main.js 315 KiB main [emitted] main
test.js 315 KiB test [emitted] test</code></pre>
<p>添加如下配置之后:</p>
<pre><code class="javascript">optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
// 下面的意思是:将从node_modules中引入的模块统一打包到一个vendors.js文件中
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
filename: 'vendors.js'
},
// 打包除上面vendors以外的公共模块
default: {
priority: -20,
reuseExistingChunks: true, // 如果该chunk已经被打包进其他模块了,这里就复用了,不打包进common.js了
filename: 'common.js'
}
}
}
}</code></pre>
<p>打包后的文件体积为</p>
<pre><code class="javascript"> Asset Size Chunks Chunk Names
common.js 308 KiB default~main~test [emitted] default~main~test
main.js 7.03 KiB main [emitted] main
test.js 7.02 KiB test [emitted] test</code></pre>
<p><strong>配置说明</strong></p>
<pre><code class="js">splitChunks: {
chunk: 'all', // all(全部), async(异步的模块),initial(同步的模块)
minSize: 3000, // 表示文件大于3000k的时候才对他进行打包
maxSize: 0,
minChunks: 1, // 当某个模块满足minChunks引用次数时,才会被打包。例如,lodash只被一个文件import,那么lodash就不会被code splitting,lodash将会被打包进 被引入的那个文件中。如果满足minChunks引用次数,lodash会被单独抽离出来,打出一个chunk。
maxAsyncRequests: 5, // 在打包某个模块的时候,最多分成5个chunk,多余的会合到最后一个chunk中。这里分析下这个属性过大过小带来的问题。当设置的过大时,模块被拆的太细,造成并发请求太多。影响性能。当设置过小时,比如1,公共模块无法被抽离成公共的chunk。每个打包出来的模块都会有公共chunk
automaticNameDelimiter: '~', // 当vendors或者default中的filename不填时,打包出来的文件名就会带~
name: true,
cashGroups: {} // 如上
}</code></pre>
<p><a>maxAsyncRequests</a></p>
<h3>Lazy Loading</h3>
<p>异步<code>import</code>的包会被单独打成一个<code>chunk</code></p>
<pre><code class="javascript">async function getComponent() {
const { default: _ } = await import(/* webpackChunkNanem:'lodash */ 'lodash')
const element = document.createElement('div')
element.innerHTML = _.join(['Dell', 'Lee'], '-')
return element
}
document.addEventListener('click', () => {
getComponent().then(element => {
document.body.appendChild(element)
})
})</code></pre>
<p><a>lazy loading</a></p>
<h3>chunk</h3>
<p>每一个<code>js</code>文件都是一个<code>chunk</code></p>
<p><code>chunk</code>是使用<code>Webpack</code>过程中最重要的几个概念之一。在Webpack打包机制中,编译的文件包括entry(入口,可以是一个或者多个资源合并而成,由html通过script标签引入)和chunk(被entry所依赖的额外的代码块,同样可以包含一个或者多个文件)。从页面加速的角度来讲,我们应该尽可能将所有的js打包到一个bundle.js之中,但是总会有一些功能是使用过程中才会用到的。出于性能优化的需要,对于这部分资源我们可以做成按需加载。</p>
<h3>打包分析</h3>
<p>打包分析:</p>
<p>安装:<code>npm install --save-dev webpack-bundle-analyzer</code></p>
<pre><code class="javascript">// package.json => scripts
"analyz": "NODE_ENV=production npm_config_report=true npm run build"</code></pre>
<pre><code class="javascript">// webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
plugins: [
new BundleAnalyzerPlugin()
]</code></pre>
<p>执行命令<code>npm run analyz</code></p>
<p>浏览器就会自动打开<code>localhost:8888</code>,分析图就会展现在你眼前</p>
<p>非常清晰直观的看出</p>
<p><img src="/Users/zhouatie/Library/Application" alt="image-20190421142354243" title="image-20190421142354243"></p>
<h3>CSS文件的代码分割</h3>
<p>我们之前写的css文件都会被打包进js文件中,要想把css单独打包成一个css文件该怎么做呢?</p>
<p>这个时候就需要用到<code>MiniCssExtractPlugin</code></p>
<p>开发环境用不到这个功能,一般都是用在生产环境中。</p>
<p>安装:<code>npm install --save-dev mini-css-extract-plugin</code></p>
<pre><code class="javascript">// webpack.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module: {
rules: [
{
test: /\.css$/,
use: [{
loader: MiniCssExtractPlugin.loader,
options: {
// 可以在此处指定publicPath
// 默认情况下,它在webpackoptions.output中使用publicPath
publicPath: '../',
// hmr: process.env.NODE_ENV === 'development',
},
}, 'css-loader']
}
]
},
plugins: [
new MiniCssExtractPlugin({
// 与webpackoptions.output中相同选项类似的选项
// 两个选项都是可选的
filename: '[name].css',
chunkFilename: '[id].css',
}),
]
// index.js
import './index.css';
console.log('haha')
// index.css
body {
background: green;
}</code></pre>
<p>这样打包之后,css会被单独打包成一个css文件。</p>
<h3>缓存</h3>
<p>目前为止,我们每次修改内容,打包出去后的文件名都不变。线上环境的文件是有缓存的。所以当你文件名不变的话,更新内容打包上线。有缓存的电脑就无法获取到最新的代码。</p>
<p>这个时候我们就会用到<code>contenthash</code></p>
<p>我们先记录配置<code>contenthash</code>之前打包的文件名。</p>
<pre><code class="javascript"> Asset Size Chunks Chunk Names
index.html 180 bytes [emitted]
main.js 3.81 KiB main [emitted] main</code></pre>
<p>接下来我们来配置下<code>contenthash</code> (就是根据你文件内容生成的hash值)</p>
<pre><code class="javascript">// webpack.config.js
output: {
path: path.resolve(__dirname, '../dist'),
filename: '[name][contenthash].js'
},</code></pre>
<p>打包完之后会在<code>main</code>后面接上<code>hash</code>值。</p>
<pre><code class="javascript"> Asset Size Chunks Chunk Names
index.html 200 bytes [emitted]
mainf5faa2d3d1e119256290.js 3.81 KiB main [emitted] main</code></pre>
<p>当你不更新内容重新打包后,<code>contenthash</code>还会维持不变。所以线上用户访问的时候就不会去服务器重新拿取代码,而是从缓存中取文件。</p>
<h4>
<code>shimming</code> (预置依赖)</h4>
<p>以<code>jquery</code>为例,代码如下</p>
<pre><code class="javascript">// index.js
import $ from 'jquery'
$('body').html('HHAHAH')
import func from './test.js'
func()
// test.js
export default function func() {
$('body').append('<h1>2</h1>')
}
</code></pre>
<p>当你不在test.js中引入<code>import $ from 'jquery'</code></p>
<p>那么浏览器访问的时候,会报</p>
<p><code>test.js:5 Uncaught ReferenceError: $ is not defined</code></p>
<p>这个时候就需要使用垫片功能</p>
<pre><code class="javascript">const webpack = require('webpack')
plugins: [
new webpack.ProvidePlugin({
$: 'jquery'
})
]</code></pre>
<p>当你加上这段代码后,模块在打包的时候,发现你使用了<code>$</code>就会在你模块顶部自动加入<code>import $ from 'jquery'</code></p>
<p><strong>其他关于<code>shimming</code>的内容参考<code>webpack</code>官网 <a>shimming</a></strong></p>
webpack4学习笔记(一)
https://segmentfault.com/a/1190000019182449
2019-05-15T01:29:27+08:00
2019-05-15T01:29:27+08:00
我是南方大汉
https://segmentfault.com/u/woshinanfangdahan
1
<h2>前言</h2>
<p>这是我花了几个星期学习webpack4的学习笔记。内容不够细,因为一些相对比较简单的,就随意带过了。希望文章能给大家带来帮助。如有错误,希望及时指出。例子都在<a href="https://link.segmentfault.com/?enc=pCh4Otte5nzSnud1AkygZA%3D%3D.XbJZdPt1q1n6OlwAh351ZdsQfI2ICrAGOD0dfzM32RHodceiVj8udus%2Bd8aze%2Ffz" rel="nofollow">learn-webpack</a>仓库上。如果你从中有所收获的话,希望你能给我的<code>github</code>点个<code>star</code>。</p>
<h3>小知识</h3>
<p><code>npm info webpack</code> 查看webpack的历史版本信息等<br><code>npm init -y</code> 跳过那些选项,默认<br>全局安装的<code>webpack</code> : <code>webpack index.js</code> 打包<br>项目中安装的<code>webpack</code>:<code> npx webpack index.js</code> 打包<br><code>script</code>中脚本打包 : <code>npm run build</code><br>命令行中打包:<code>npx webpack index.js -o bundle.js</code> 入口是<code>index.js</code>, 出口是<code>bundle.js</code><br><code>webpack4</code>设置<code>mode:production</code> 会压缩代码 <code>development</code> 就不压缩代码<br>打包<code> output</code>里面<code>[name].js loader</code>中的<code>name</code>变量 其实就是<code>entry:{main: index.js} 中的key => main</code></p>
<h3>source-map</h3>
<p><code>devtool: source-map</code><br><code>source-map</code>: <code>dist</code>文件夹里会多生成个<code>map</code>后缀文件,这样页面报错的时候,点击报错后面的地址,会跳转到代码写的地方。而不会跳转到打包后的代码里。<br><code>inline-source-map</code>: 不会新生成.map文件,会插入到打包的文件底部<br><code>cheap-inline-source-map</code>: 因为<code>inline-source-map</code>报错会告诉你第几行第几个字符。前面加上<code>cheap</code>的话 只会告诉你第几行<br><code>cheap-module-inline-source-map</code>: 本来map只会映射打包出来的index.js跟业务代码中的关系。第三方引入库报错映射不到。中间加了module这个参数就可以了。比如<code>loader</code>也会有<code>source-map</code><br>开发的时候建议使用:<code>cheap-module-eval-source-map</code>,<code>eval</code>表示不会独立生成map文件,而是打包进代码里。<br>一般<code>development</code>环境用 <code>cheap-module-eval-source-map</code><br><code>production</code>环境用<code>cheap-module-source-map</code></p>
<h3>loader</h3>
<pre><code class="javascript">// style.css
body {
color: red;
}</code></pre>
<p>当你想给项目添加样式的时候,<code>import ./style.css</code>导入<code>css</code>,并执行打包命令的时候。页面并报错</p>
<pre><code class="javascript">ERROR in ./style.css 1:5
Module parse failed: Unexpected token (1:5)
You may need an appropriate loader to handle this file type.</code></pre>
<p>这个时候就需要使用<code>loader</code>来编译了。<br>安装:<code>npm i style-loader css-loader -D</code><br>为什么还需要安装<code>style-loader</code>呢?因为<code>style-loader</code>会将你的样式通过<code>style</code>标签插入到页面中<br>配置<code>webpack.config.js</code></p>
<pre><code class="javascript">// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode: "development",
entry: './index.js',
output: {
filename: 'bundle.js'
},
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
},
plugins: [
new HtmlWebpackPlugin()
]
}</code></pre>
<p>上面使用了<code>HtmlWebpackPlugin</code>是因为我用它来自动生成<code>index.html</code>方便页面访问</p>
<pre><code class="javascript">// package.json
"scripts": {
"build": "webpack --config webpack.config.js"
},</code></pre>
<p>执行<code>npm run build</code>进行打包,访问<code>dist</code>目录下的<code>index.html</code>,可以看到页面显示成功。<br><strong><code>loader</code>执行顺序是从下到上,右到左。</strong></p>
<h3>webpack-dev-server</h3>
<h5>webpack-dev-server</h5>
<blockquote>
<code>webpack-dev-server</code>会对你的代码进行打包,将打包的内容放到内存里面,并不会自动给你打包放进dist里。<br><code>webpack —watch</code>页面会刷新下,内容会自动更新<br><code>webpack-dev-server</code>会自动更新当前页面<br><code>webpack-dev-server</code>, 现在的<code>webpack-dev-server</code>比以前好多了 <code>vue-cli3</code> 和<code>react</code>都是用这个了</blockquote>
<pre><code class="javascript">devServer: {
contentBase: './dist', // 有这个就够了,
open: true, // 自动打开浏览器
port: 8080, // 端口不必填
proxy: {'/api': http://localhost:3000}
}</code></pre>
<h5>启动服务来热更新</h5>
<p><code>npm install express webpack-dev-middleware -D</code><br>在<code>output</code>中添加<code>publicPath</code></p>
<pre><code class="javascript">const express = require('express')
const webpack = require('webpack')
const webpackDevMiddleWare = require('webpack-dev-middleware')
const config = require('./webpack.config.js')
const complier = webpack(config) // 帮我们做编译的东西,webpack传入config之后会申请一个编译器
app.use(webpackDevMiddleware(complier, {
publicPath: config.output.publicPath, // 意思是只要文件改变了,就重新运行
}))
const app = express()
app.listen(3000, () => {
console.log('server is running 3000')
})</code></pre>
<blockquote>现在这样子写太麻烦了(vue-cli2也是如此)。因为以前版本<code>webpack-dev-server</code>还不够强大,现在不一样了。非常的强大了。</blockquote>
<h3>Hot Module Replacement</h3>
<blockquote>热替换,就是不会刷新整个页面。当不使用热更新的时候,操作一些功能,新增了三个元素,修改样式页面自动刷新后,刚才新增的元素会消失。如果开启了热替换,那么原先的dom会还在。</blockquote>
<pre><code class="javascript">const webpack = require('webpack')
// ....
devServer: {
contentBase: './dist',
open: true,
hot: true,
hotOnly: true // 以防hot失效后,页面被刷新,使用这个,hot失效也不刷新页面
},
// ...
plugins: [
new webpack.HotModuleReplacementPlugin()
]</code></pre>
<pre><code class="javascript">import number from './number.js'
if (module.hot) { // 如果热更新存在
// 监听的文件改变,会触发后面的回调方法
module.hot.accept('./number', () => {
// dosomething
})
}</code></pre>
<blockquote>为什么修改了css文件不需要写module.hot。而写js需要写呢,因为css-loader已经自动帮你处理了。</blockquote>
<h3>babel</h3>
<h4>基本用法</h4>
<p>将高版本的js代码转换成低版本的js代码。比如ie浏览器不兼容es6,需要使用babel把es6代码把js转换成低版本的js代码。<br>安装:<code>npm install --save-dev babel-loader @babel/core</code></p>
<pre><code class="javascript">module: {
rules: [
{ test: /\.js$/, exclude: /node_modules/, loader: "babel-loader" }
]
}</code></pre>
<p><code>babel-loader</code>并不会帮助你把es6语法转换成低级的语法。它只是帮助打通webpack跟babel之间的联系。<br>转换成es5的语法:<br>安装:<code>npm install @babel/preset-env --save-dev</code><br><code>@babel/preset-env</code>包含了es6转换成es5的所有规则。</p>
<pre><code class="javascript">module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader",
options: {
"presets": ["@babel/preset-env"]
}
}
]
}</code></pre>
<p>如果还需要降到更低版本就得使用<code>babel-polyfill</code><br>安装:<code>npm install --save @babel/polyfill</code><br>页面顶部引入<code>import "@babel/polyfill";</code>就可以将高级版本语法转换成低级语法。但是直接<code>import</code>会让打包后的文件非常大。<br>这个时候就需要再配置<code>webpack.config.js</code><br><strong>useBuiltIns</strong></p>
<pre><code class="javascript">{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader",
options: {
// "presets": ["@babel/preset-env"]
// 当你做babel-polyfill往浏览器填充的时候,根据业务代码用到什么加什么,并不会全部写入,
// 数组中,后面的对象是对数组前面的babel做配置
"presets": [["@babel/preset-env", {
useBuiltIns: 'usage'
}]]
}
}</code></pre>
<p><strong>如果开发一个第三方库,类库。使用<code>babel-polyfill</code>会注入到全局,污染到全局环境</strong>。<br>安装:<code>npm install --save-dev @babel/plugin-transform-runtime</code><br>安装:<code>npm install --save @babel/runtime</code></p>
<pre><code class="javascript">{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader",
options: {
"plugins": ["@babel/plugin-transform-runtime"]
}
}</code></pre>
<p>当你要添加配置时</p>
<pre><code class="javascript">{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader",
options: {
"plugins": [["@babel/plugin-transform-runtime", {
"absoluteRuntime": false,
"corejs": false,
"helpers": true,
"regenerator": true,
"useESModules": false
}]]
}
}</code></pre>
<p>如果你要将corejs赋值为2<br>安装:<code>npm install --save @babel/runtime-corejs2</code></p>
<pre><code class="javascript">{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader",
options: {
"plugins": [["@babel/plugin-transform-runtime", {
"absoluteRuntime": false,
"corejs": 2,
"helpers": true,
"regenerator": true,
"useESModules": false
}]]
}
}</code></pre>
<p><code>@babel/plugin-transform-runtime</code>不会污染到全局环境。<br>当babel配置非常多的时候,可以将他们放到<code>.babelrc</code>文件里<br>在根目录下创建<code>.babelrc</code>文件<br>将<code>options</code>里的代码放到这个文件中,如下:</p>
<pre><code class="javascript">{
"plugins": [["@babel/plugin-transform-runtime", {
"absoluteRuntime": false,
"corejs": 2,
"helpers": true,
"regenerator": true,
"useESModules": false
}]]
}</code></pre>
<h4>react中应用babel</h4>
<p>安装:<code>npm install --save-dev @babel/preset-react</code><br>往刚刚的<code>.babelrc</code>文件中添加配置</p>
<pre><code class="javascript">// presets对应一个数组,如果这个值需要做配置,那么这个值在包装进一个数组,放到第一项,第二项是个对象,用于描述该babel
{
"presets": [
["@babel/preset-env", {
"useBuiltIns": "usage"
}],
"@babel/preset-react"
]
}</code></pre>
<p><strong>注意:转换是先下后上,就是使用preset-react转换react代码,再用preset-env将js代码转换为es5代码</strong></p>
vue ssr 从认识到构建一个工程项目(一)
https://segmentfault.com/a/1190000018827195
2019-04-10T23:36:49+08:00
2019-04-10T23:36:49+08:00
我是南方大汉
https://segmentfault.com/u/woshinanfangdahan
1
<h2>vue ssr入门</h2>
<h3>前言</h3>
<p>近期需要接手一个vue ssr项目,由于本人之前没有写过ssr,只是稍微了解了点。所以跟着官网学了下,并整理出了这篇学习笔记。方便自己以后对vue ssr知识的回顾。好记性不如烂笔头。</p>
<h3>介绍</h3>
<p>相信大家在看到这篇文章之前,都知道ssr是什么了。SSR,英文全称叫 Server(服务) side(端) rendering (渲染)哈哈☺</p>
<p>那么究竟什么是服务器端渲染?</p>
<blockquote>Vue.js 是构建客户端应用程序的框架。默认情况下,可以在浏览器中输出 Vue 组件,进行生成 DOM 和操作 DOM。然而,也可以将同一个组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将这些静态标记"激活"为客户端上完全可交互的应用程序。<p>服务器渲染的 Vue.js 应用程序也可以被认为是"同构"或"通用",因为应用程序的大部分代码都可以在<strong>服务器</strong>和<strong>客户端</strong>上运行。</p>
</blockquote>
<p>如果你问我为什么使用ssr呢?(<a>具体可参考官网</a>)</p>
<blockquote><ul>
<li>有利于seo。</li>
<li>更快的内容到达时间 (time-to-content),特别是对于缓慢的网络情况或运行缓慢的设备。大体可以理解为渲染出页面时间,csr比ssr多了个js下载时间。因为ssr一开始加载下来就渲染出来了,然后在下载激活html的js。csr是下载完在渲染。</li>
</ul></blockquote>
<h3>正文</h3>
<h4>基本用法</h4>
<p>ssr主要依靠两个包<code>vue-server-renderer</code>和 <code>vue</code>(两个版本必须匹配)</p>
<p>安装: <code>npm install vue vue-server-renderer --save</code></p>
<h5>入门配置</h5>
<h6>ssr最简易配置</h6>
<pre><code class="javascript">// server.js
const server = require('express')()
const Vue = require('vue');
const renderer = require('vue-server-renderer').createRenderer();
server.get('*', (req, res) => {
const context = {
url: req.url
}
const app = new Vue({
template: `<div>${context.url}</div>`
})
renderer.renderToString(app, (err, html) => {
if (err) {
res.status(500).end('Internal Server Error')
return
}
res.end(`
<!DOCTYPE html>
<html lang="en">
<head><title>Hello</title></head>
<body>${html}</body>
</html>
`)
})
})
server.listen(8080)</code></pre>
<p><code>node server.js</code> 浏览器输入localhost:8080访问该ssr页面</p>
<p>这时候你可以看到,无论你输入什么路径,页面文本都会显示出你的路径</p>
<p><img src="/img/bVbq9Yn?w=1292&h=500" alt="clipboard.png" title="clipboard.png"></p>
<h6>ssr使用模板</h6>
<p>当你在渲染 Vue 应用程序时,renderer 只从应用程序生成 HTML 标记 (markup)。在这个示例中,我们必须用一个额外的 HTML 页面包裹容器,来包裹生成的 HTML 标记。纯客户端渲染的时候,会有一个模板,会插入你打包后的一些文件等。那么ssr会不会也有这种模板呢?当然会有。</p>
<ul>
<li>
<p>首先在根目录下新建一个<code>index.template.html</code>文件</p>
<pre><code class="html"><!DOCTYPE html>
<html lang="en">
<head><title>Hello</title></head>
<body>
<!--vue-ssr-outlet-->
</body>
</html></code></pre>
<p>注意了 --跟vue或者outlet跟--之间不能用空格。注释 -- 这里将是应用程序 HTML 标记注入的地方。</p>
</li>
<li>
<p>接下来,修改下刚才的server.js文件后如下</p>
<pre><code class="javascript">const server = require('express')()
const Vue = require('vue');
const renderer = require('vue-server-renderer').createRenderer({
template: require('fs').readFileSync('./index.template.html', 'utf-8')
});
server.get('*', (req, res) => {
const context = {
url: req.url
}
const app = new Vue({
template: `<div>${context.url}</div>`
})
renderer.renderToString(app, (err, html) => {
if (err) {
res.status(500).end('Internal Server Error')
return
}
res.end(html)
})
})
server.listen(8080)</code></pre>
<p>就是在createRenderer中多加一个参数 template(读取模板文件),并传递给createRenderer方法</p>
<p>模板还支持插值</p>
<pre><code class="html"><html>
<head>
<!-- 使用双花括号(double-mustache)进行 HTML 转义插值(HTML-escaped interpolation) -->
<title>{{ title }}</title>
<!-- 使用三花括号(triple-mustache)进行 HTML 不转义插值(non-HTML-escaped interpolation) -->
{{{ meta }}}
</head>
<body>
<!--vue-ssr-outlet-->
</body>
</html></code></pre>
<p>我们可以通过传入一个"渲染上下文对象",作为 <code>renderToString</code> 函数的第二个参数,来提供插值数据:</p>
<pre><code class="javascript">const context = {
title: 'hello',
meta: `
<meta ...>
<meta ...>
`
}
renderer.renderToString(app, context, (err, html) => {
// 页面 title 将会是 "Hello"
// meta 标签也会注入
})</code></pre>
</li>
</ul>
<h6>编写通用代码</h6>
<p>我们以往的纯浏览器渲染都是把js下载到本地执行的。上述代码你会发现都是用的同一个Vue构造函数,但是想对该构造函数做特殊处理时,就会对其他用户造成污染。因此,我们不应该直接创建一个应用程序实例,而是应该暴露一个可以重复执行的工厂函数,为每个请求创建新的应用程序实例:</p>
<pre><code class="javascript">// 修改原先代码如下
-const Vue = require('vue');
+const createApp = require('./app.js')
- const app = new Vue({
- template: `<div>${context.url}</div>`
- })
+ const { app } = createApp(context)
// 新增app.js
const Vue = require('vue');
module.exports = function createApp(context) {
const app = new Vue({
template: `<div>${context.url}</div>`
})
return { app }
}</code></pre>
<p>这样,每次访问该服务器的时候,都会生成一个新的vue实例。同样的规则也适用于 router、store 和 event bus 实例。你不应该直接从模块导出并将其导入到应用程序中,而是需要在 <code>createApp</code> 中创建一个新的实例,并从根 Vue 实例注入。</p>
<h3>参考</h3>
<p><a>Vue SSR 指南</a></p>
<p><a>vue 服务端渲染ssr</a></p>
<p><a href="https://segmentfault.com/a/1190000016637877#articleHeader0">带你五步学会Vue SSR</a></p>
javaScript排序算法学习笔记
https://segmentfault.com/a/1190000018467862
2019-03-12T02:00:41+08:00
2019-03-12T02:00:41+08:00
我是南方大汉
https://segmentfault.com/u/woshinanfangdahan
2
<h2>javaScript排序算法学习笔记</h2>
<pre><code class="javascript">// 用于创建数组
function createNonSortedArray(size) {
var array = new ArrayList();
for( var i = size; i>0; i--) {
array.insert(i);
}
return array;
}
function ArrayList() {
var array = [];
this.insert = function(item) {
console.log(item, 'insert');
array.push(item);
}
this.toString = function() {
console.log('tostring');
return array.join();
}
/*
* 冒泡排序
* 冒泡排序比较任何两个相邻的项,如果第一个比第二个大,则交换它们。
* 元素项向上移动至正确的顺序,就好像气泡升至表面一样,冒泡排序因此得名。
* 第一轮比过之后最后一个就一定是最大的 无需再比较。所以下次要 - i
*/
this.bubbleSort = function() {
var length = array.length;
for(var i = 0;i<length;i++) {
for(var j = 0;j<length - 1 - i; j++) {
if (array[j] > array[j + 1]) {
swap(array, j, j+1);
}
}
}
}
/*
* 选择排序
* 选择排序算法是一种原址比较排序算法。选择排序大致的思路是找到数据结构中的最小值
* 并将其放置在第一位,接着找到第二小的值并将其放在第二位,以此类推。比如,第一个
* 的时候,会遍历后面所有想跟其比较,找到最小的更其交换。所以第一个此时一定是最小的。
* 随意第二个的时候,只会循环后面的几个。如果找到一个比第二个更小的,那么交换位置。
*/
this.selectionSort = function() {
var length = array.length,
indexMin;
for(var i = 0;i< length - 1;i++) {
indexMin = i;
for(var j = i;j<length;j++) {
if (array[indexMin]>array[j]) {
indexMin = j;
}
}
if (i !== indexMin) {
swap(array, i, indexMin);
}
}
}
/*
* 插入排序
* 插入排序每次排第一个数组项,以此方式构建最后的排序数组。假定第一项已经排序了,接着,
* 它和第二项进行比较,第二项是应该待在原位还是插到第一项之前呢?这样,头两项就已正确
* 排序,接着和第三项比较(它是该插入到第一、第二还是第三位置呢?),以此类推。
* 简而言之,就是遍历数组的每一项,拿这一项去跟前面的项比较,如果比他小就插入到它前面。
*/
this.insertionSort = function() {
var length = array.length,
j,temp;
for (var i = 1;i<length;i++) {
j = i;
temp = array[i];
while(j>0 && array[j-1] > temp) {
array[j] = array[j - 1];
j--;
}
array[j] = temp;
}
}
// 归并排序
// 归并排序是一种分治算法。其思想是将原始数组切分成较小的数组,直到每个
// 小数组只有一个位置,接着将小数组归并成较大的数组,直到最后一个排序完毕的大数组。
this.mergeSort = function() {
array = mergeSortRec(array);
}
var mergeSortRec = function(array) {
var length = array.length;
if (length === 1) {
return array;
}
var mid = Math.floor(length / 2),
left = array.slice(0, mid),
right = array.slice(mid, length);
return merge(mergeSortRec(left), mergeSortRec(right));
}
var merge = function(left, right) {
var result = [],
il = 0,
ir = 0;
// 完成下列操作的前提是left、right数组均已经完成。所以采用递归的形式
// 在数组长度为1的时候先开始排序,然后在通过merge left与right数组
while(il < left.length && ir < right.length) {
if (left[il] < right[ir]) {
result.push(left[il++]);
} else {
result.push(right[ir++]);
}
}
// 上面是将left与right数组排完序,那么其中之一数组必然为空,
// 下面的操作就是将剩下的right或者left全部推入result数组中
while(il < left.length) {
result.push(left[il++]);
}
while(ir < right.length) {
result.push(right[ir++]);
}
return result;
}
// 快速排序
// 首先,从数组中选择中间一项作为主元
// 创建两个指针,左边一个指向数组的第一项,右边一个指向数组的最后一个项。
// 移动左指针直到我们找到一个比主元大的元素,接着,移动右指针直到找到一个
// 比主元小的元素,然后交换他们,重复这个过程,直到左指针超过右指针。这个
// 过程将使得比主元小的值都排在主元之前,而比主元大的值都排在主元之后。这一步
// 叫做划分操作。
// 接着,算法对划分后的小数组(较主元小的值组成的子数组,以及较主元大的值
// 组成的子数组)重复之前的两个步骤,直到数组已完全排序。
// 简而言之,先分治,不断的细化下去,到最后一个数组无法再交换位置进行排序位置
this.quickSort = function() {
quick(array, 0, array.length - 1);
}
var quick = function(array, left, right) {
var index;
if (array.length > 1) {
index = partition(array, left, right);
if (left < index - 1 ) {
quick(array, left, index - 1);
}
if (index < right) {
quick(array, index, right);
}
}
}
var partition = function(array, left, right) {
var pivot = array[Math.floor((left + right) / 2)],
i = left,
j = right;
while(i <= j) {
while(array[i] < pivot) {
i++;
}
while(array[j] > pivot) {
j--;
}
if (i <= j) {
swap(array, i, j);
i++;
j--;
}
}
return i;
}
var swap = function(array, index1, index2) {
var aux = array[index1];
array[index1] = array[index2];
array[index2] = aux;
}
}
var array = createNonSortedArray(9);
console.log(array.toString());
array.bubbleSort();
// array.selectionSort();
// array.insertionSort();
// array.mergeSort();
// array.quickSort();
console.log(array.toString());</code></pre>
javascript数据结构学习笔记
https://segmentfault.com/a/1190000018374312
2019-03-04T02:49:31+08:00
2019-03-04T02:49:31+08:00
我是南方大汉
https://segmentfault.com/u/woshinanfangdahan
3
<h2>数据结构</h2>
<h3>数组</h3>
<h4>方法</h4>
<pre><code class="javascript">//一、数组
var arr = [];
// 添加元素
arr.push(1, 2); // [1,2]
// 开头插入
arr.unshift(0); // [0, 1, 3]
// 尾部删除
arr.pop(); // [0, 1]
// 头部删除
arr.shift(); // [1]
// 数组合并
[1].concat([2]) // [1,2]</code></pre>
<h4>迭代器</h4>
<ul>
<li>
<code>every</code> every方法会迭代数组中每个元素,直到返回false。</li>
<li>
<code>some</code> some和every类似,不过some方法会迭代数组的每个元素,直到函数返回true</li>
<li>
<code>forEach</code> 和for循环的结果相同</li>
<li>
<code>map</code> 返回新的数组 <code>[1,2].map(o => o * 2) // [2,4]</code>
</li>
<li>
<code>filter</code> 返回新的数组 <code>[1,2].filter(o => o > 1) // [2]</code>
</li>
<li>
<code>reduce</code> <code>[1,2].reduce((result, current) => result + current) // 3</code>
</li>
<li>
<code>for of</code> <code>for (let n of numbers) { console.log((n % 2 === 0) ? 'even' : 'odd')};</code>
</li>
<li>
<p><code>entries</code></p>
<pre><code class="javascript">const numbers = [1,2,3];
let aEntries = numbers.entries(); // 得到键值对的迭代器
console.log(aEntries.next().value); // [0, 1] 位置0的值为1
console.log(aEntries.next().value); // [1, 2] 位置1的值为2
console.log(aEntries.next().value); // [2, 3] 位置2的值为3</code></pre>
</li>
<li>
<p><code>keys</code></p>
<pre><code class="javascript">const numbers = [1,2,3];
console.log(Object.keys(numbers)); // ['0','1','2'];</code></pre>
</li>
<li>
<p><code>values</code></p>
<pre><code class="javascript">const numbers = [1,2,3];
console.log(Object.values(numbers)); // [1,2,3]</code></pre>
</li>
<li><code>Array.from</code></li>
<li><code>Array.of</code></li>
<li><code>fill</code></li>
<li><code>copyWithin</code></li>
<li><code>sort</code></li>
<li><code>find</code></li>
<li><code>findIndex</code></li>
<li><code>includes</code></li>
</ul>
<h3>栈</h3>
<blockquote>栈是一种遵从<strong>后进先出</strong>原则的有序集合</blockquote>
<h4>实现</h4>
<pre><code class="javascript">function Stack() {
let items = [];
// 向栈添加元素
this.push = function(element) {
items.push(element);
}
// 从栈移除元素
this.pop = function() {
return items.pop();
};
// 查看栈顶元素
this.peek = function() {
return items[item.length - 1];
}
// 检查栈是否为空
this.isEmpty = function() {
return items.length == 0;
}
this.size = function() {
return items.length;
};
// 清空和打印栈元素
this.clear = function() {
items = [];
};
this.print = function() {
console.log(items.toString());
};
}
</code></pre>
<h4>用栈解决问题</h4>
<p>存储访问过的任务或路径、撤销的操作等。</p>
<h3>队列</h3>
<blockquote>队列是遵循FIFO(First In First Out, 先进先出,也称为先来先服务)</blockquote>
<h4>实现</h4>
<pre><code class="javascript">function Queue() {
let items = [];
// 向队列添加元素
this.enqueue = function(element) {
items.push(element);
};
// 从队列移除元素
this.dequeue = function() {
return items.shift();
};
// 查看队列头元素
this.front = function() {
return items[0];
};
// 检查队列是否为空
this.isEmpty = function() {
return items.length == 0;
};
this.size = function() {
return items.length;
};
// 打印队列元素
this.print = function() {
console.log(items.toString());
};
}</code></pre>
<h3>链表</h3>
<blockquote>链表村粗有序的元素集合,但不同于数组,链表中的元素在内存中并不是连续放置的。每个元素由一个存储元素本身的节点和一个指向下一个元素的引用(也称指针或链接)组成。<br>相对于传统的数组,链表的一个好处在于,添加或移除元素的时候不需要移动其他元素。然而,链表需要使用指针,因此实现链表时需要额外注意。数组的另一个细节是可以直接访问任何位置的任何元素,而要想访问链表中间的一个元素,需要从起点(表头)开始迭代列表直到找到所需的元素。</blockquote>
<h4>实现</h4>
<pre><code class="javascript">function LinkedList() {
let Node = function(element) {
this.element = element;
this.next = null;
};
let length = 0;
let head = null;
// 向链表尾部追加元素
this.append = function(element) {
let node = new Node(element),
current;
if (head === null) {
head = node;
} else {
current = head;
// 循环列表,直到找到最后一项
while (current.next) {
current = current.next;
}
// 找到最后一项,将其next赋为node,建立链接
current.next = node;
}
length++; // 更新列表的长度
}
// 从链表中移除元素
this.removeAt = function() {
// 检查越界值
if (position > -1 && position < length) {
let current = head,
previous,
index = 0;
// 移除第一项
if (position === 0) {
head = current.next;
} else {
while (index++ < position) {
previous = current;
current = current.next;
}
// 将previous 与 current的下一项链接起来: 跳过current,从而移除它
previous.next = current.next;
}
length--;
return current.element;
} else {
return null;
}
}
// 在任意位置插入元素
this.insert = function(position, element) {
// 检查越界值
if (position >= 0 && position <= length) {
let node = new Node(element),
current = head,
previous,
index = 0;
if (position === 0) { // 在第一个位置添加
node.next = current;
head = node;
} else {
while (index++ < position) {
previous = current;
current = current.next;
}
node.next = current;
previous.next = node;
}
length++; // 更新列表的长度
return true;
} else {
return false;
}
}
// toString方法
this.toString = function() {
let current = head,
string = '';
while (current) {
string += current.element + (current.next ? 'n' : '');
current = current.next;
}
return string;
}
// indexOf 方法
this.indexOf = function(elment) {
let current = head,
index = 0;
while(current) {
if (element === current.element) {
return index;
}
index++;
current = current.next;
}
return -1;
}
// remove 方法
this.remove = function(elment) {
let index = this.indexOf(element);
return this.removeAt(index);
}
// isEmpty 方法
this.isEmpty = function() {
return length == 0;
}
// size 方法
this.size = function() {
return length;
}
// getHead 方法
this.getHead = function() {
return head;
}
}
</code></pre>
<h4>双向链表(留给大家自己思考)</h4>
<h3>集合</h3>
<blockquote>集合是由一组无序且唯一(即不能重复)的项组合的。这个数据结构使用了与有限集合相同的数学概念,但应用在计算机科学的数据结构中。</blockquote>
<pre><code class="javascript">function Set() {
let items = {};
// has 方法
this.has = function(value) {
return items.hasOwnProperty(value);
};
// add 方法
this.add = function(value) {
if (!this.has(value)) {
items[value] = value;
return true;
}
return false;
}
// remove 方法
this.remove = function(value) {
if (this.has(value)) {
delete items[value];
return true;
}
return false;
}
// clear 方法
this.clear = function() {
items = {};
}
// size 方法
this.size = function() {
return Object.keys(items).length;
}
// values 方法
this.values = function() {
let values = [];
for (let i = 0, keys = Object.keys(items); i< keys.length; i++) {
values.push(items[keys[i]]);
}
return values;
}
// 并集
this.union = function(otherSet) {
let unionSet = new Set();
let values = this.values();
for (let i = 0; i < values.length; i++) {
unionSet.add(values[i]);
}
values = otherSet.values();
for (let i = 0; i < values.length; i++) {
unionSet.add(values[i]);
}
return unionSet;
}
// 交集
this.intersection = function(otherSet) {
let intersectionSet = new Set();
let values = this.values();
for (let i = 0;i<values.length; i++) {
if (otherSet.has(values[i])) {
intersectionSet.add(values[i]);
}
}
return intersectionSet;
}
// 差集
this.difference = function(otherSet) {
let differenceSet = new Set();
let values = this.values();
for (let i = 0; i< values.length; i++) {
if (!otherSet.has(values[i])) {
differenceSet.add(values[i]);
}
}
return differenceSet;
}
// 子集
this.subset = function(otherSet) {
if (this.size() > otherSet.size()) {
return false;
} else {
let values = this.values();
for (let i = 0;i< values.length;i++) {
if (!otherSet.has(values[i])) {
return false;
}
}
return true;
}
}
}</code></pre>
<h3>字典和散列表</h3>
<h4>实现</h4>
<pre><code class="javascript">function Dictionary() {
var items = {};
// has 和 set 方法
this.has = function(key) {
return items.hasOwnProperty(key);
}
this.set = function(key, value) {
item[key] = value;
}
// delete 方法
this.delete = function(key) {
if (this.has(key)) {
delete items[key];
return true;
}
return false;
}
// get 和 values 方法
this.get = function(key) {
return this.has(key) ? items[key] : undefined;
}
this.values = function() {
var values = [];
for(var k in items) {
if (this.has(k)) {
values.push(items[k]);
}
}
return values;
}
// clear 方法
this.clear = function() {
items = {};
}
// size 方法
this.size = function() {
return Object.keys(items).length;
}
// keys 方法
this.keys = function() {
return Object.keys(items);
}
// getItems 方法
this.getItems = function() {
return items;
}
}</code></pre>
<h4>散列表</h4>
<blockquote>HashTable类 也叫 HashMap类,它是Dictionary类的一种散列表是实现方式。<br>散列算法的作用是尽可能快的在数据结构中找到一个值。</blockquote>
<pre><code class="javascript">function HashTable() {
var table = [];
var loseloseHashCode = function(key) {
var hash = 0;
for (var i = 0; i< key.length; i++) {
hash += key.charCodeAt(i);
}
return hash % 37;
}
this.put = function(key, value) {
var position = loseloseHashCode(key);
console.log(position + ' - ' + key);
table[position] = value;
}
this.get = function(key) {
return table[loseloseHashCode(key)];
}
this.remove = function(key) {
table[loseloseHashCode(key)] = undefined;
}
}
</code></pre>
<h4>Map类</h4>
<blockquote>es6 新增了Map类</blockquote>
<pre><code class="javascript">var map = new Map();
map.set('a', 'b');
console.log(map.has('a')); // true
console.log(map.size()); // 输出1
console.log(map.keys()); // ['a']
console.log(map.values()); // ['b'];
// 和Dictionary类不同,es6的Map类的values方法和keys方法都返回Iterator,而不是值或键构成的数组。</code></pre>
<h4>es6 --- WeakMap类 和 WeakSet类</h4>
<ul>
<li>WeakMap 和 WeakSet类没有entries keys values等方法</li>
<li>只能用对象作为键</li>
</ul>
<pre><code class="javascript">var map = new WeakMap();
var obj = {name: 'a'};
map.set(obj, 'b');
console.log(map.has(obj)); // 输出true
console.log(map.get(obj)); // 输入'b'
map.delete(obj);</code></pre>
<h3>树</h3>
<blockquote>一个树结构包含一系列存在父子关系的节点。每个节点都有一个父节点(除了顶部的第一个节点)以及零个或多个子节点;</blockquote>
<h4>二叉树和二叉搜索树</h4>
<pre><code class="javascript">function BinarySearchTree() {
var Node = function(key) {
this.key = key;
this.left = null;
this.right = null;
}
var root = null;
var insertNode = function(node, newNode) {
if (newNode.key < node.key) {
if (node.left === null) {
node.left = newNode;
} else {
insertNode(node.left, newNode);
}
} else {
if (node.right === null) {
node.right = newNode;
} else {
insertNode(node.right, newNode);
}
}
}
// 向树中插入一个键
this.insert = function(key) {
var newNode = new Node(key);
if (root = null) {
root = newNode;
} else {
insertNode(root, newNode);
}
}
var inOrderTraverseNode = function(node, callback) {
if (node !== null) {
inOrderTraverseNode(node.left, callback);
callback(node.key);
inOrderTraverseNode(node.right, callback);
}
}
// 中序遍历
this.inOrderTraverse = function(callback) {
inOrderTraverseNode(root, callback);
}
var preOrderTraverseNode = function(node, callback) {
if (node !== null) {
callback(node.key);
preOrderTraverseNode(node.left, callback);
preOrderTraverseNode(node.right, callback);
}
}
// 先序遍历
this.preOrderTraverse = function(callback) {
preOrderTraverseNode(root, callback);
}
var postOrderTraverseNode = function(node, callback) {
if (node !== null) {
postOrderTraverseNode(node.left, callback);
postOrderTraverseNode(node.right, callback);
callback(node.key);
}
}
// 后序遍历
this.postOrderTraverse = function(callback) {
postOrderTraverseNode(root, callback);
}
// 搜索最小值
this.min = function() {
return minNode(root);
}
var minNode = function(node) {
if (node) {
while( node && node.left !== null) {
node = node.left;
}
return node.key;
}
return null;
}
// 搜索最大值
this.max = function() {
return maxNode(root);
}
var maxNode = function(node) {
if (node) {
while(node && node.right !== null) {
node = node.right;
}
return node.key;
}
return null;
}
// 搜索一个特定的值
this.search = function(key) {
return searchNode(root, key);
}
var searchNode = function(node, key) {
if (node === null) {
return false;
}
if (key < node.key) {
return searchNode(node.left, key);
} else if (key > node.key) {
return searchNode(node.right, key);
} else {
return true;
}
}
// 移除一个节点
this.remove = function(key) {
root = removeNode(root, key);
}
var removeNode = function(node, key) {
if (node === null) {
return null;
}
if (key < node.key) {
node.left = removeNode(node.left,key);
return node;
} else if (key > node.key) {
node.right = removeNode(node.right,key);
return node;
} else { // 键等于node.key
// 第一种情况--一个叶节点
if (node.left === null && node.right === null) {
node = null;
return node;
}
// 第二种情况--一个只有一个子节点的节点
if (node.left === null) {
node = node.right;
return node;
} else if (node.right === null) {
node = node.left;
return node;
}
// 第三种情况---- 一个有两个子节点的节点
var aux = findMinNode(node.right);
node.key = aux.key;
node.right = removeNode(node.rihgt, aux.key);
return node;
}
var findMinNode = function(node) {
while (node && node.left !== null) {
node = node.left;
}
return node;
}
}
}</code></pre>
<h4>自平衡树(AVL)</h4>
<blockquote>当树很深的时候,添加移除和搜索某个节点时引起一些性能问题。</blockquote>
<pre><code class="JavaScript">var heightNode = function(node) {
if (node === null) {
return -1;
} else {
return Math.max(heightNode(node.left), heightNode(node.right)) + 1;
}
}
var rotationRR = function(node) {
var tmp = node.right;
node.right = tmp.left;
tmp.left = node;
return tmp;
}
var rotationLL = function(node) {
var tmp = node.left;
node.left = tmp.right;
tmp.right = node;
return tmp;
}
var rotationLR = function(node) {
node.left = rotationRR(node.left);
return rotationLL(node);
}
var rotationRL = function(node) {
node.right = rotationLL(node.right);
return rotationRR(node);
}
var insertNode = function(node, element) {
if (node === null) {
node = new Node(element);
} else if (element < node.key) {
node.left = insertNode(node.left, element);
if (node.left !== null) {
// 确认是否需要平衡
if ((heightNode(node.left) - heightNode(node.right) > 1)) {
if (element < node.left.key) {
node = rotationLL(node);
} else {
node = rotationLR(node);
}
}
}
} else if (element > node.key) {
node.right = insertNode(node.right, element);
if (node.right !== null) {
// 确认是否需要平衡
if ((heightNode(node.right) - heightNode(node.left) > 1)) {
if (element > node.right.key) {
node = rotationRR(node);
} else {
node = rotationRL(node);
}
}
}
}
return node;
}
</code></pre>
<h3>图</h3>
<blockquote>图是网络结构的抽象模型,图是一组由边连接的节点(或顶点)。学习图是重要的,因为任何关系都可以用图来表示</blockquote>
<pre><code class="javascript">function Graph() {
var vertices = [];
var adjList = new Dictionary();
this.addVertex = function(v) {
vartices.push(v);
adjList.set(v, []);
}
this.addEdge = function(v, w) {
addList.get(v).push(w);
addList.get(w).push(v);
}
this.toString = function() {
var s = '';
for (var i = 0; i< vertices.length;i++) {
s += vertices[i] + ' -> ';
var neighbors = adjList.get(vertices[i]);
for (var j = 0;j<neighbors.length;j++) {
s += neighbors[j] + ' ';
}
s += '\n';
}
return s;
}
// 广度优先搜索
var initializeColor = function() {
var color = [];
for( var i = 0;i< vertices.length; i++) {
color[vertices[i]] = 'white';
}
return color;
}
this.bfs = function(v, callback) {
var color = initializeColor(),
queue = new Queue();
queue.enqueue(v);
while(!queue.isEmpty()) {
var u = queue.dequeue(),
neighbors = adjList.get(u);
color[u] = 'grey';
for(var i = 0;i<neighbors.length;i++) {
var w = neighbors[i];
if (color[w] === 'white') {
color[w] = 'grey';
queue.enqueue(w);
}
}
color[u] = 'black';
if (callback) {
callback();
}
}
}
// 使用BFS寻找最短路径
this.BFS = function(v) {
var color = initializeColor(),
queue = new Queue(),
d = [];
pred = [];
queue.enqueue(v);
for( var i = 0;i< vertices.length;i++) {
d[vertices[i]] = 0;
pred[vertices[i]] = null;
}
while(!queue.isEmpty()) {
var u = queue.dequeue(),
neighbors = adjList.get(u);
color[u] = 'grey';
for( i = 0;i<neighbors.length;i++) {
var w = neighbors[i];
if (color[w] === 'white') {
color[w] = 'grey';
d[w] = d[u] + 1;
pred[w] = u;
queue.enqueue(w);
}
}
color[u] = 'black';
}
return {
distances: d,
predecessors: pred
}
}
// 深度优先遍历
var dfsVisit = function(u, color, callback) {
color[u] = 'grey';
if (callback) {
callback(u);
}
var neighbors = adjList.get(u);
for(var i = 0;i<neighbors.length;i++) {
var w = neighbors[i];
if (color[w] === 'white') {
dfsVisit(w, color, callback);
}
}
color[u] = 'black';
}
this.dfs = function(callback) {
var color = initializeColor();
for(var i = 0; i< vertices.length; i++) {
if (color[vertices[i]] === 'white') {
dfsVisit(vertices[i], color, callback);
}
}
}
// 探索深度优先算法
var time = 0;
this.DFS = function() {
var color = nitializeColor(),
d = [],
f = [],
p = [],
time = 0;
for( var i = 0; i< vertices.length; i++) {
f[vertices[i]] = 0;
d[vertices[i]] = 0;
p[vertices[i]] = null;
}
for (i = 0; i< vertices.length; i++) {
if (color[vertices[i]] === 'white') {
DFSVisit(vertices[i], color, d, f, p);
}
}
return {
discovery: d,
finished: f,
predecessors: p
}
}
var DFSVisit = function(u, color, d, f, p) {
console.log('discovered ' + u);
color[u] = 'grey';
d[u] = ++time;
var neighbors = adjList.get(u);
for(var i = 0;i<neighbors.length; i++) {
var w = neighbors[i];
if (color[w] === 'white') {
p[w] = u;
DFSVisit(w, color, d, f, p);
}
}
color[u] = 'black';
f[u] = ++time;
console.log('explored ' + u);
}
}
</code></pre>
阻止中文输入法输入拼音的时候触发input事件
https://segmentfault.com/a/1190000016988944
2018-11-12T21:49:11+08:00
2018-11-12T21:49:11+08:00
我是南方大汉
https://segmentfault.com/u/woshinanfangdahan
0
<h2>阻止中文输入法输入拼音的时候触发input事件</h2>
<h3>前言</h3>
<p>最近看element-ui源码的时候看到el-input发现的。这个少见的事件。</p>
<h4>
<code>compositionstart</code>、<code>compositionend</code>事件(MDN解释)</h4>
<p><code>compositionstart</code>事件触发于一段文字的输入之前(类似于 keydown 事件,但是该事件仅在若干可见字符的输入之前,而这些可见字符的输入可能需要一连串的键盘操作、语音识别或者点击输入法的备选词)。<br>当文本段落的组成完成或取消时, <code>compositionend</code> 事件将被触发 (具有特殊字符的触发, 需要一系列键和其他输入, 如语音识别或移动中的字词建议)。</p>
<pre><code class="javascript">/**
* @param {Element} elem input元素
* @param {Function} callback input事件绑定的回调
*/
function inputEvent(elem, callback) {
let isOnComposition = false;
elem.addEventListener('compositionstart', function(event) {
isOnComposition = true;
})
elem.addEventListener('compositionend', function(event) {
isOnComposition = false;
const val = event.target.value;
handleInput(val);
})
elem.addEventListener('input', function(event) {
const val = event.target.value;
handleInput(val);
})
function handleInput(val) {
if (isOnComposition) return;
callback(val);
}
}
window.onload = function() {
const input = document.getElementById("input");
inputEvent(input, function(val) {
console.log(val);
})
}</code></pre>
配置nginx解决vue路由history模式下刷新404问题
https://segmentfault.com/a/1190000016358990
2018-09-11T20:01:36+08:00
2018-09-11T20:01:36+08:00
我是南方大汉
https://segmentfault.com/u/woshinanfangdahan
0
<p>在vue路由模式为history的时候,刷新页面会出现404问题。我们只需要在服务器配置如果URL匹配不到任何静态资源,就跳转到默认的index.html。</p>
<pre><code class="javascript">
server {
listen 8105; // 表示你nginx监听的端口号
root /home/admin/sites/vue-nginx/dist; // vue打包后的文件夹dist目录
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
}
</code></pre>
vue响应式原理
https://segmentfault.com/a/1190000016333054
2018-09-10T09:39:37+08:00
2018-09-10T09:39:37+08:00
我是南方大汉
https://segmentfault.com/u/woshinanfangdahan
14
<h2>vue响应式原理</h2>
<h3>initState</h3>
<p>new Vue() => _init() => initState:</p>
<pre><code class="javascript">function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}</code></pre>
<p>判断该vue实例是否存在<code>props</code>、<code>methods</code>、<code>data</code>、<code>computed</code>、<code>watch</code>进行调用相应的初始化函数</p>
<h4><strong>initProps与initData</strong></h4>
<p>主要工作是调用<code>defineProperty</code>给属性分别挂载get(触发该钩子时,会将当前属性的dep实例推入当前的Dep.target也就是当前watcher的deps中即它订阅的依赖,Dep.target下文会讲到。且该dep实例也会将当前watcher即观察者推入其subs数组中)、set方法(通知该依赖subs中所有的观察者watcher去调用他们的update方法)。</p>
<h4><strong>initComputed</strong></h4>
<p>它的作用是将computed对象中所有的属性遍历,并给该属性new一个computed watcher(计算属性中定义了个dep依赖,给需要使用该计算属性的watcher订阅)。也会通过调用<code>defineProperty</code>给computed挂载get(get方法)、set方法(set方法会判断是否传入,如果没传入会设置成noop空函数)<br><code>computed</code>属性的get方法是下面函数的返回值函数</p>
<pre><code class="javascript">function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
watcher.depend()
return watcher.evaluate()
}
}
}</code></pre>
<p>注意其中的<code>watcher.depend()</code>,该方法让用到该属性的watcher观察者订阅该watcher中的依赖,且该计算属性watcher会将订阅它的watcher推入他的subs中(当计算属性值改变的时候,通知订阅他的watcher观察者)<br><code>watcher.evaluate()</code>,该方法是通过调用watcher的get方法(其中需要注意的是watcher的get方法会调用pushTarget将之前的Dep.target实例入栈,并设置Dep.target为该computed watcher,被该计算属性依赖的响应式属性会将该computed watcher推入其subs中,所以当被依赖的响应式属性改变时,会通知订阅他的computed watcher,computed watcher 再通知订阅该计算属性的watcher调用update方法),get方法中调用计算属性key绑定的handler函数计算出值。</p>
<h4><strong>initWatch</strong></h4>
<p>该watcher 为user watcher(开发人员自己在组件中自定义的)。<br>initWatch的作用是遍历watch中的属性,并对每个watch监听的属性调用定义的$watch</p>
<pre><code class="javascript">Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true // 代表该watcher是用户自定义watcher
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
cb.call(vm, watcher.value)
}
return function unwatchFn () {
watcher.teardown()
}
}</code></pre>
<p>代码中调用new Watcher的时候,也会同render watcher一样,执行下watcher的get方法,调用<code>pushTarget</code>将当前user watcher赋值给Dep.target,get()中<code>value = this.getter.call(vm, vm)</code>这个语句会触发该自定义watcher监听的响应式属性的get方法,并将当前的user watcher推入该属性依赖的subs中,所以当user watcher监听的属性set触发后,通知订阅该依赖的watcher去触发update,也就是触发该watch绑定的key对应的handler。然后就是调用popTarget出栈并赋值给Dep.target。</p>
<h3>$mount</h3>
<p>initState初始化工作大致到这里过,接下去会执行$mount开始渲染工作<br>$mount主要工作:new了一个渲染Watcher,并将updateCompent作为callback传递进去并执行</p>
<pre><code class="javascript">updateComponent = () => {
vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)</code></pre>
<p>三种watcher中new Watcher的时候,只有computed watcher不会一开始就执行它的get()方法。$mount里面new的这个render watcher会调用<code>get()</code>方法,调用<code>pushTarget</code>将当前render watcher赋值给Dep.target。接下去重头戏来了,调用<code>updateComponent</code>,该方法会执行<code>vm._update(vm._render(), hydrating)</code>,其中render函数会触发html中使用到的响应式属性的get钩子。get钩子会让该响应式属性的依赖实例dep将当前的render watcher推入其subs数组中,所以当依赖的响应式属性改变之后,会遍历subs通知订阅它的watcher去调用update()。</p>
<h3>例子</h3>
<p>可能大家对watcher和dep调来调去一头雾水,我讲个实例</p>
<pre><code class="javascript"><div id="app">
<div>{{a}}</div>
<div>{{b}}</div>
</div>
new Vue({
el: "#app",
data() {
return {
a:1,
}
},
computed:{
b() {
return a+1
}
},
})</code></pre>
<p>我直接从渲染开始讲,只讲跟dep跟watcher有关的<br><strong>$mount</strong>:new一个渲染watcher(watcher的get方法中会将渲染watcher赋值给Dep.target)的时候会触发 <code>vm._update(vm._render(), hydrating)</code>,render的时候会获取html中用到的响应式属性,上面例子中先用到了a,这时会触发a的get钩子,其中<code>dep.depend()</code>会将当前的渲染watcher推入到a属性的dep的subs数组中。<br>接下去继续执行,访问到b(b是计算属性的值),会触发计算属性的get方法。计算属性的get方法是调用<code>createComputedGetter</code>函数后的返回函数<code>computedGetter</code>,<code>computedGetter</code>函数中会执行<code>watcher.depend()</code>。Watcher的depend方法是专门留给computed watcher使用的。刚才上面说过了除了computed watcher,其他两种watcher在new 完之后都会执行他们的get方法,那么computed watcher在new完之后干嘛呢,它会new一个dep。回到刚才说的专门为computed watcher开设的方法<code>watcher.depend()</code>,他的作用是执行<code>this.dep.depend()</code>(computed watcher定义的dep就是在这里使用到的)。<code>this.dep.depend()</code>会让当前的渲染watcher订阅该计算属性依赖,该计算属性也会将渲染watcher推入到它自己的subs([render watcher])中,当计算属性的值修改之后会通知subs中的watcher调用<code>update()</code>,所以计算属性值变了页面能刷新。回到前面说的触发b计算属性的get钩子那里,get钩子最后会执行<code>watcher.evaluate()</code>,<code>watcher.evaluate()</code>会执行computed watcher的<code>get()</code>方法。这时候重点来了,会将Dep.target(render watcher)推入targetStack栈中(存入之后以便待会儿取出继续用),然后将这个计算属性的computed watcher赋值给Dep.target。get方法中<code>value = this.getter.call(vm, vm)</code>,会执行computed属性绑定的handler。如上面例子中return a + 1。使用了a那么就一定会触发a的get钩子,get钩子又会调用<code>dep.depend()</code>,dep.depend()会让computed watcher将dep存入它的deps数组中,a的dep会将<strong>当前的Dep.target(computed watcher)存入其subs数组中,当前例子中a的subs中就会是[render watcher,computed watcher]</strong>,所以a值变化会遍历a的subs中的watcher调用<code>update()</code>方法,html中用到的a会刷新,计算属性watcher调用<code>update()</code>方法会通知他自己的subs([render watcher])中render watcher去调用update方法,html中用到的计算属性b才会刷新dom(这里提个醒,我只是粗略的讲,计算属性依赖的属性变化后他不一定会触发更新,他会比较计算完之后的值是否变化)。computed watcher的get()方法最后会调用<code>popTarget()</code>,将之前存入render watcher出栈并赋值给Dep.target,这时候我例子中targetStack就变成空数组了。render watcher的get方法执行到最后也会出栈,这时候会将Dep.target赋值会空。</p>
浏览器http缓存
https://segmentfault.com/a/1190000016152384
2018-08-26T14:47:15+08:00
2018-08-26T14:47:15+08:00
我是南方大汉
https://segmentfault.com/u/woshinanfangdahan
2
<h2>浏览器http缓存</h2>
<h4>浏览器http缓存大致流程图</h4>
<p><img src="https://zhouatie.github.io/xminds/http-cache/http-cache.png" alt="浏览器http缓存" title="浏览器http缓存"></p>
<h4>缓存好处</h4>
<ul>
<li>缓解服务器压力(不用每次去请求资源);</li>
<li>提升性能(打开本地资源速度当然比请求回来再打开要快得多);</li>
<li>减少带宽消耗(我相信你可以理解);</li>
</ul>
<h4>缓存种类</h4>
<ol>
<li>
<p>浏览器缓存</p>
<ul><li>浏览器对于缓存的处理是根据第一次请求资源时返回的响应头来确定的。</li></ul>
</li>
<li>代理服务器缓存</li>
<li>网关缓存</li>
<li>数据库缓存</li>
</ol>
<h4>通用首部字段</h4>
<h5>cache-control</h5>
<h6>缓存请求指令</h6>
<table>
<thead><tr>
<th>指令</th>
<th>参数</th>
<th>说明</th>
</tr></thead>
<tbody>
<tr>
<td>no-cahce</td>
<td>无</td>
<td>强制向源服务器再次验证</td>
</tr>
<tr>
<td>no-store</td>
<td>无</td>
<td>不缓存请求或响应的任何内容</td>
</tr>
<tr>
<td>max-age = [秒]</td>
<td>必需</td>
<td>期望在指定时间内的响应仍有效</td>
</tr>
<tr>
<td>max-state( = [秒])</td>
<td>可省略</td>
<td>接收已过期的响应</td>
</tr>
<tr>
<td>min-fresh = [秒]</td>
<td>必需</td>
<td>期望在指定时间内发响应仍有效</td>
</tr>
<tr>
<td>no-transform</td>
<td>无</td>
<td>代理不可更改媒体类型</td>
</tr>
<tr>
<td>only-if-cached</td>
<td>无</td>
<td>从缓存获取资源</td>
</tr>
<tr>
<td>cache-extension</td>
<td>-</td>
<td>新指令标记[token]</td>
</tr>
</tbody>
</table>
<h6>缓存响应指令</h6>
<table>
<thead><tr>
<th>指令</th>
<th>参数</th>
<th>说明</th>
</tr></thead>
<tbody>
<tr>
<td>public</td>
<td>无</td>
<td>可向任意方提供响应的缓存</td>
</tr>
<tr>
<td>private</td>
<td>可省略</td>
<td>仅向特定用户返回响应</td>
</tr>
<tr>
<td>no-cahce</td>
<td>可省略</td>
<td>缓存前必需先确认其有效性</td>
</tr>
<tr>
<td>no-store</td>
<td>无</td>
<td>不缓存请求或响应的任何内容</td>
</tr>
<tr>
<td>no-transform</td>
<td>无</td>
<td>代理不可更改媒体类型</td>
</tr>
<tr>
<td>must-revalidate</td>
<td>无</td>
<td>可缓存但必须再想源服务器进行确认</td>
</tr>
<tr>
<td>proxy-revalidate</td>
<td>无</td>
<td>要求中间缓存服务器对缓存的响应有效性再进行确认</td>
</tr>
<tr>
<td>max-age = [秒]</td>
<td>必需</td>
<td>响应的最大Age值</td>
</tr>
<tr>
<td>s-maxage = [秒]</td>
<td>必需</td>
<td>公共缓存服务器响应的最大Age值</td>
</tr>
<tr>
<td>cache-extension</td>
<td>-</td>
<td>新指令标记[token]</td>
</tr>
</tbody>
</table>
<h6>表示是否能缓存的指令</h6>
<p>public指令</p>
<p><code>Cache-Control: public</code></p>
<blockquote>当使用public指令时,则明确表明其他用户也可利用缓存。<br>private指令</blockquote>
<p><code>Cache-Control: private</code></p>
<blockquote>当使用private指令时,响应只以特定的用户作为对象,这与public指定的行为相反。<p>缓存服务器会对该特定用户提供资源缓存的服务,对于其他用户发送过来的请求,代理服务器则不会返回缓存</p>
</blockquote>
<p>no-cache指令</p>
<p><code>Cache-Control: no-cache</code></p>
<blockquote>使用no-cache指令的目的是为了防止从缓存中返回过期的资源。<p>客户端发送的请求如果包含no-cache指令,则表示客户端将不会缓存过的响应。于是,“中间”的缓存服务器必须把客户端请求转发给源服务器。如果服务器返回的响应中包含no-cache指令,那么缓存服务器不能对资源进行缓存。源服务器以后也将不再对缓存服务器请求中提出的资源有效性进行确认,且禁止其对响应资源进行缓存操作。</p>
</blockquote>
<p>no-store指令</p>
<p><code>Cache-Control: no-store</code></p>
<blockquote>当使用no-store指令时,暗示请求(和对应的响应)或响应中包含机密信息。因此,该指令规定缓存不能再本地存储请求或响应的任一部分。</blockquote>
<p>s-maxage指令</p>
<p><code>Cache-Control: s-maxage=604800 (单位 :秒)</code></p>
<blockquote>s-maxage指令的功能和max-age指令的相同,他们的不同点是s-maxage指令适用于供多位用户使用的公共缓存服务器。</blockquote>
<p>max-age指令</p>
<p><code>Cache-Control: max-age = 604800 (单位 :秒)</code></p>
<blockquote>当客户端发送的请求中包含max-age指令时,如果判定缓存资源的缓存时间数值比指定时间的数值更小,那么客户端就接收缓存的资源。另外,当指定max-age值为0,那么缓存服务器通常需要将请求转发给源服务器。<br>当服务器返回的响应中包含max-age指令时,缓存服务器将不对资源的有效性再作确认,而max-age数值代表资源保存为缓存的最长时间。<br>应用HTTP/1.1版本的缓存服务器遇到同时存在Expires首部字段的情况时,会优先处理max-age指令,而忽略掉Expires首部字段。而HTTP/1.0版本的缓存服务器的情况却相反,max-age指令会被忽略掉。</blockquote>
<p>min-fresh指令</p>
<p><code>Cache-Control: min-fresh=60(单位:秒)</code></p>
<blockquote>min-fresh指令要求缓存服务器返回至少还未过指定时间的缓存资源。比如,当指定min-fresh为60秒后,在这60秒以内如果有超过有效期限的资源都无法作为响应返回了。</blockquote>
<p>max-stale指令</p>
<p><code>Cache-Control:max-stale=3600(单位:秒)</code></p>
<blockquote>使用max-stale可指示缓存资源,即使过期也照常接收。<p>如果指令未指定参数值,那么无论经过多久,客户端都会接收响应;如果指令中指定了具体数值,那么即使过期,只要扔处于max-stale指定的时间内,仍旧会被客户端接收。</p>
</blockquote>
<p>only-if-cached指令</p>
<p><code>Cache-Control: only-if-cached</code></p>
<blockquote>使用only-if-cached指令表示客户端仅在缓存服务器本地缓存目标资源的情况下才会要求其返回。换言之,该指令要求缓存服务器不重新加载响应,也不会再次确认资源有效性。若发生请求缓存服务器的本地缓存无响应,则返回状态码504 Gateway Timeout。</blockquote>
<p>must-revalidate指令</p>
<p><code>Cache-Control: must-revalidate</code></p>
<blockquote>使用must-revalidate指令,代理会向源服务器再次验证即将返回的响应缓存目前是否仍然有效。<p>若代理无法连通源服务器再次获取有效资源的话,缓存必须给客户端一条504(Gateway Timeout)状态码。</p>
<p>另外,使用must-revalidate指令会忽略请求的max-stale指令(即使已经在首部使用了max-stale,也不会再有效果)。</p>
</blockquote>
<p>proxy-revalidate指令</p>
<p><code>Cache-Control: proxy-revalidate</code></p>
<blockquote>proxy-revalidate指令要求所有的缓存服务器在接收到客户端带有该指令的请求返回响应之前,必须再次验证缓存的有效性。</blockquote>
<p>no-transform指令</p>
<p><code>Cache-Control: no-transform</code></p>
<blockquote>使用no-transform指令规定无论是在请求还是响应中,缓存都不能改变实体主体的媒体类型。</blockquote>
<p>Cache-Control扩展</p>
<p>cache-extension token</p>
<p><code>Cache-Control: private, community="UCI"</code></p>
<blockquote>通过cache-extension标记(token),可以扩展Cache-Control首部字段内的指令。<p>如上例,Cache-Control首部字段本身没有community这个指令。借助extension tokens 实现了该指令的添加。如果缓存服务器不能理解community这个新指令,就会直接忽略。因此,extension tokens仅对能理解它的缓存服务器来说是有意义的。</p>
</blockquote>
<h6>Date</h6>
<p>首部字段Date表明创建HTTP报文的日期和时间。<br>HTTP/1.1协议使用在RFC1123中规定的日期时间的格式,如下示例。<br><code>Date: Tue, 03 Jul 2012 04:40:59 GMT</code><br>之前的HTTP协议版本中使用在RFC850中定义的格式,如下所示。<br><code>Date: Tue, 03-Jul-12 04:40:59 GMT</code><br>除此之外,还有一种格式。它与C标准库内的asctime()函数的输出格式一致。<br><code>Date: Tue Jul 03 04:40:59 2012</code></p>
<h6>Pragma</h6>
<p>Pragma是HTTP/1.1之前版本的历史遗留字段,仅作为与HTTP/1.0的向后兼容而定义。</p>
<p><code>Pragma: no-cache</code></p>
<blockquote>该首部字段属于通用首部字段,但只用在客户端发送的请求中。客户端会要求所有的中间服务器不返回缓存的资源。<p>所有的中间服务器如果都能以HTTP/1.1为基准,那直接采用Cache-Control: no-cache指定缓存的处理方式是最为理想的。但要整体掌握全部中间服务器使用的HTTP协议版本中却是不现实的。因此,发送的请求会同时含有下面两个首部字段。</p>
</blockquote>
<pre><code class="javascript">Cache-Control: no-cache
Pragma: no-cache</code></pre>
<h4>请求首部字段</h4>
<p>If-Match/If-Modified-Since/If-None-Match/If-Range/If-Unmodified-Since</p>
<blockquote>形如If-xxx这种形式的请求首部字段,都可称为条件请求。服务器接收到附带条件的请求后,只有判断指定条件为真时,才会执行请求。</blockquote>
<h5>If-Match</h5>
<blockquote>只有当If-Match的值与服务器上实体的Etag值一致时,服务器才会接受请求。反之,则返回状态码412 Precondition Failed的响应。</blockquote>
<h5>If-Modified-Since</h5>
<blockquote>如果在If-Modified-Since字段指定的日期时间后,资源发生了更新,服务器会接受请求。比如请求首部<code>If-Modified-Since: Thu, 15 Apr 2004 00:00:00 GMT</code>,而服务器上的<code>Last-Modified: Sun 29 Aug 2004 14:03:05 GMT</code>,因为是在2004年4月15日之后更新过的资源,所以服务器会接受请求。</blockquote>
<h5>If-None-Match</h5>
<blockquote>只有在If-None-Match的字段值与Etag值不一致时,可处理该请求。与If-Match首部字段的作用相反。</blockquote>
<h5>If-Range</h5>
<blockquote>If-Range HTTP 请求头字段用来使得 Range 头字段在一定条件下起作用:当字段值中的条件得到满足时,Range 头字段才会起作用,同时服务器回复206 部分内容状态码,以及Range 头字段请求的相应部分;如果字段值中的条件没有得到满足,服务器将会返回 200 OK 状态码,并返回完整的请求资源。</blockquote>
<h5>If-Unmodified-Since</h5>
<blockquote>首部字段<code>If-Unmodified-Since</code>和首部字段<code>If-Modified-Since</code>的作用相反。他的作用是告知服务器,指定的请求资源只有在字段值内指定的日期时间之后,未发生更新的情况下,才能处理请求。如果在指定的日期时间后发生了更新,则以状态码412 Precondition Faied作为相应返回。</blockquote>
<h4>响应首部字段</h4>
<p>响应首部字段是由服务器端向客户端返回响应报文中所使用的字段,用于补充响应的附加信息、服务器信息、以及对客户端的附加要求等信息。</p>
<h5>Age</h5>
<blockquote>首部字段Age能告知客户端,源服务器在多久前创建了响应。字段值的单位为秒。<p>若创建该响应的服务器是缓存服务器,Age值是指缓存后的响应再次发起认证到认证完成的时间值。代理创建响应时必须加上首部字段Age。Age消息头的值通常接近于0。表示此消息对象刚刚从原始服务器获取不久;其他的值则是表示代理服务器当前的系统时间与此应答消息中的通用消息头 Date 的值之差。</p>
</blockquote>
<h5>Etag</h5>
<blockquote>首部字段Etag能告知客户端实体标识。它是一种可将资源以字符串形式做唯一性标识的方式。服务器会为每份资源分配对应的Etag值。<p>另外,当资源更新时,Etag值也需要更新。生成Etag值时,并没有统一的算法规则,而仅仅是由服务器来分配。</p>
<p><strong>强Etag值</strong>:不论实体发生多么细微的变化都会改变其值。<code>Etag: "usagi-1234"</code></p>
<p><strong>弱Etag值</strong>:弱Etag值只用于提示资源是否相同。只有资源发生了根本改变,产生差异时才会改变Etag值。这时,会在字段值最开始处附加W/。</p>
</blockquote>
<p><code>Etag:W/"usagi-1234"</code>。</p>
<h4>实体首部字段</h4>
<h5>Expires</h5>
<p><code>Expires: Wed, 04 Jul 2012 08:26:05 GMT</code></p>
<blockquote>首部字段<code>Expires</code>会将资源失效的日期告知客户端。缓存服务器在接受到含有首部字段<code>Expires</code>的响应后,会以缓存来应答请求,在<code>Expires</code>字段值指定的时间之前,响应的副本会一直被保存。当超过指定的时间后,缓存服务器在请求发送过来时,会转向源服务器请求资源。<p>源服务器不希望缓存服务器对资源缓存时,最好在<code>Expires</code>字段内写入与首部字段Date相同的时间值。</p>
<p>但是,当首部字段<code>Cache-Control</code>有指定<code>max-age</code>指令时,比起首部字段<code>Expires</code>,会优先处理max-age指令。</p>
</blockquote>
<h5>Last-Modified</h5>
<p><code>Last-Modified: Wed, 23 May 2012 09:59:55 GMT</code></p>
<blockquote>首部字段<code>Last-Modified</code>指明资源最终修改时间。一般来说,这个值就是Request-URI指定资源被修改的时间。但类似使用CGI脚本进行动态数据处理时,该值有可能会变成数据最终修改时的时间。</blockquote>
<h3>参考与推荐</h3>
<p>《图解HTTP》<br><a href="https://link.segmentfault.com/?enc=tji2cVgQNaAiYx98KYj%2FQA%3D%3D.7EG06I3JfGmihTaGiEXi%2FgM4hULf%2BLGpTFQy1ECcHVGFaJLzeIf0wmmazQ4dnVdUr5sKiMrajxE8hjMjSyVLUA%3D%3D" rel="nofollow">缓存详解</a><br><a href="https://link.segmentfault.com/?enc=yrTF8xRqzBRz6ONluvtVOg%3D%3D.N4tMlX1tfT5OZRO3Awhe%2ByqT%2Fm%2BkMs6Spmy7r%2F2aioe6hWz5TIFWn%2FNHNOHDg9%2B8rj2%2BS02vJnp%2FVcO%2BFlCDbXTKCtt2jVmu1ufsZ1Y8oKU%3D" rel="nofollow">浅谈web缓存</a><br><a href="https://segmentfault.com/a/1190000012573337">浅谈浏览器缓存</a></p>
思维导图—你不知道的JavaScript中卷
https://segmentfault.com/a/1190000015996577
2018-08-14T00:12:45+08:00
2018-08-14T00:12:45+08:00
我是南方大汉
https://segmentfault.com/u/woshinanfangdahan
19
<h3><a href="https://link.segmentfault.com/?enc=vIxO53dUMnvATG%2BHsA0VaQ%3D%3D.SCD0TChiXmhIRw7YW43zp4tQrhaYqrdBovanooEbMZoYw5sLopyQjCs3zM7ZzJ6g" rel="nofollow">xmind地址</a></h3>
<h3>预览</h3>
<p><img src="/img/bVbfhBm?w=1525&h=3460" alt="图片描述" title="图片描述"></p>
原生js实现日期选择器插件
https://segmentfault.com/a/1190000015973790
2018-08-12T01:30:55+08:00
2018-08-12T01:30:55+08:00
我是南方大汉
https://segmentfault.com/u/woshinanfangdahan
18
<h2>原生js实现日期选择器插件</h2>
<h3>前言</h3>
<p>距离自己上次写插件差不多半年了。公司技术栈都是用框架,调解下口味就写了此原生插件。因为本意是想兼容到ie9就不用es6语法写了。前段时间在看vue源码受了点启发,本插件有点接近数据驱动视图更新的响应式渲染。如果希望有更多功能的,可在下方留言,我尽量扩展!如果你有需要或者喜欢的话,可以给我github来个star 😆</p>
<blockquote>
<a href="https://link.segmentfault.com/?enc=KHhkW4tE9PaE09otuA0uNw%3D%3D.DwB0vDf0aiI1gGxK9tEs1yz0siFjbnJaeb8pqm00isTw%2BpCKOEeFzE9kgtaOvdxKLVHed7LLqIit%2FB9qYmn%2BPA%3D%3D" rel="nofollow">仓库地址</a><p><a href="https://link.segmentfault.com/?enc=vDn6KG3RRxvkvGfsnBwIIA%3D%3D.WvvJpO5fab6YB%2FaH2rw8TQf9dM1W%2B82hczqNT4qYPLjffK3oszhsT6wvjkbnP5yZT4H0Di%2FjOQjY6LDxm2Ab7A%3D%3D" rel="nofollow">在线预览</a></p>
</blockquote>
<h4>预览</h4>
<p><img src="/img/remote/1460000015973793?w=386&h=500" alt="拾色器" title="拾色器"></p>
<h4>准备</h4>
<p>首先在页面中引入css、js文件(文件在我的github,如何引入可看github示例html)</p>
<p>在页面中写上如下代码:</p>
<pre><code class="javaScript">Calendar.create({
classN: 'calendar-item', // 这里的calendar-item可随意填 不需要跟我一样
callBack: function(bindElem, dateObj) {
// bindElem: 该控件绑定的元素
// dateObj: 选中的年、月、日 如: {year: 2018, month: 8, date: 12}
// 用户可通过bindElem和dateObj搞事情啦 😆
bindElem.innerHTML = dateObj.year + '-' + dateObj.month + '-' + dateObj.date;
}
})</code></pre>
<blockquote>
<strong><code>String: classN</code></strong>:参数填入你要绑定日期控件的元素。本插件初始化的时候,会根据用户提供的<code>classN</code>类名生成相应个数<p><strong><code>Function: callBack</code></strong>:<code>bindElem</code>: 该控件绑定的元素,<code>dateObj</code>: 选中的年、月、日 如: <code>{year: 2018, month: 8, date: 12}</code>。通过返回参数,让用户在回调函数中通过回调参数做操作,给用户更高的自由度。<br><strong>如果需要更多回调方法,我会尽量扩展</strong></p>
</blockquote>
<h3>结尾</h3>
<p>如有什么功能需要增加的,可在评论区留言,我尽量满足。如有什么疏忽或错误,希望您指出。我会尽早修改,以免误导他人。</p>
思维导图—描述vue执行机制
https://segmentfault.com/a/1190000015867323
2018-08-02T22:46:42+08:00
2018-08-02T22:46:42+08:00
我是南方大汉
https://segmentfault.com/u/woshinanfangdahan
47
<h3><a href="https://link.segmentfault.com/?enc=UA%2FHxADdFAYV4LjmAWMyVQ%3D%3D.e0lHvWUZ2rEfntA%2FneZqwYkZcqsWyDoa%2BTjGj3f2%2BBNCyfDFC0reQUZBDJXqi%2B2Oljs1iDyE5JVxuXfo9bEiIA%3D%3D" rel="nofollow">xmind地址</a></h3>
<p><img src="/img/remote/1460000015870677?w=3126&h=3144" alt="vue.png" title="vue.png"></p>
<h3>参考(排序按照看过的时间顺序)</h3>
<p><a href="https://link.segmentfault.com/?enc=TgwoXYEjrKdbNnb4bc5kxQ%3D%3D.HpZwT8qsfWE%2Bp%2FO4kitnIbrbB5QwLoxdH1DtK2WfGzn29gI8lj395G7k3TpdHfGM1M6ww8mtinko9aoRwLTq2JVjxmXCXq2pJESnKUskt2yze9wFW3UdK7%2FYXpLd4eKD" rel="nofollow">剖析 Vue.js 内部运行机制</a><br><a href="https://link.segmentfault.com/?enc=leWgE8ZPni4%2BMcZB0xLj%2BQ%3D%3D.Leggw%2F0EFU3QouR9vvBBkzve%2FsdnRHQx%2BETwcRZAGIivMdebt4zSWN5nQDV4Kv3e" rel="nofollow">Vue.js 技术揭秘</a><br><a href="https://segmentfault.com/a/1190000015440980">Vue源码阅读 - 文件结构与运行机制</a><br><a href="https://link.segmentfault.com/?enc=YfPMYibrDfMs7jMo1%2F%2BJhw%3D%3D.vmhyYmJDQvXvxKwpwTMbd5XBwrlQxznPQpJsnJRbEz9d69NLwnWE1x2CqSNkbX8OGRng1vJZyMqPm2Hz2s6Gug%3D%3D" rel="nofollow">【大型干货】手拉手带你过一遍vue部分源码</a><br><a href="https://link.segmentfault.com/?enc=qaXvaWLN%2Fs6oesyzQUml4w%3D%3D.DIdRIrG9cwOW3sw1w%2FED9ulDeuifKLvkhybBlq09%2FeRHeabHibe7bA6OTsXBh3tr" rel="nofollow">当面试官问你Vue响应式原理,你可以这么回答他</a><br><a href="https://link.segmentfault.com/?enc=neZqww93UxcX2z131X9gng%3D%3D.2jl%2BreHqeBWZodr557gqFNJsBogr2HoDT%2ByKgVuUhAmSvGwLrSCPzhSjlWyWF1sh" rel="nofollow">Vue源码剖析——render、patch、updata、vnode</a>(这篇挺不错的)</p>
变量命名规范
https://segmentfault.com/a/1190000014621403
2018-04-26T23:36:07+08:00
2018-04-26T23:36:07+08:00
我是南方大汉
https://segmentfault.com/u/woshinanfangdahan
6
<h2>变量命名规范</h2>
<h3>前言</h3>
<p>好的命名规范可以让你不用天天为取名字而苦恼、找bug时,更快的定位到bug在哪个位置</p>
<ol>
<li>组件的命名和它的功能相配套</li>
<li>不与其他业务组件重名,让人一眼就区分</li>
<li>不一定要好听酷炫,但是实用.让开发者产生条件反射,看到命名就会想到这个组件的实用场景</li>
<li>易写易记,短小却精炼,不繁琐</li>
</ol>
<h4>BEM</h4>
<p>.nav某一块展示/功能区域 (div)<br>.nav__item这块展示/功能区域(div)里面的某个元素,比如: nav__item<br>.nav__item--hide/ .nav__item--open 某个元素或者某个块的状态</p>
<h4>不要加敏感词汇</h4>
<p>我曾经给一个元素取了个class为advertisement,后来测试人员发现页面上这块元素不见了。后来发现360浏览器开启去广告模式,直接把这个div给删了。</p>
<h4>函数的命名规范</h4>
<ul>
<li>拼写准确 比如我的confirm与confrim 把函数未执行归咎于代码逻辑问题</li>
<li>
<p>使用正常的时态</p>
<ul><li>特别是代码中状态的变量或者函数的命名,比如 onXxxxStarted 表示xxx已经启动了,isConnecting表示正在连接。正确的时态可以给使用者传递准确的信息。</li></ul>
</li>
<li>
<p>函数和属性的命名是有区别的</p>
<ul><li><strong>如果是函数,建议使用动宾结构</strong></li></ul>
</li>
</ul>
<p>动宾结构就是 doSomething,这样的函数命名含义明确<br>比如: openFile, setName, addNumber...</p>
<pre><code>- **如果是属性命名,建议使用定语+名词**</code></pre>
<p>比如 fileName, maxLength, textSize</p>
<ul>
<li>不要单词+拼音混合使用<br>比如:useJiFen,huKouNumber.. 缺乏美感不说,可读性大幅度降低。</li>
<li>谨慎使用缩写<br>除非是约定俗成已经被广泛使用的缩写,否则老老实实用完整拼写。典型的反面例子: count->cnt, manager->mgr password->pwd button->btn无论我们使用eclipse 或者intellij, 都有很好的自动完成功能,名字长一点没关系的,可读性更重要。</li>
</ul>
<h4>命名的语义话(动词、名词的区分)</h4>
<h4>Vue 组件命名</h4>
<blockquote>Ant.design 的 React 组件是下面这样的时候,我感觉到一种自由的味道。首先,组件名可以使用原生 HTML 标签名,意味着再也不用较劲脑汁去规避原生 HTML 标签了。另外,这些组件都使用了首字母大写标签名,使它们很容易地与原生小写的 HTML 标签区分。</blockquote>
<pre><code class="javascript">ReactDOM.render(
<div>
<Button type="primary">Primary</Button>
<Input placeholder="Basic usage" />
<Select defaultValue=".com" style={{ width: 70 }}>
<Option value=".com">.com</Option>
<Option value=".jp">.jp</Option>
<Option value=".cn">.cn</Option>
<Option value=".org">.org</Option>
</Select>
</div>,
mountNode
);</code></pre>
<h4>基础组件命名</h4>
<p>应用特定样式和约定的基础组件 (也就是展示类的、无逻辑的或无状态的组件) 应该全部以一个特定的前缀开头,比如 Base、App 或 V。</p>
<pre><code class="javascript">**反例**
components/
|- button.vue
|- loading.vue
|- slide.vue
**正例**
components/
|- BaseButton.vue
|- BaseLoading.vue
|- BaseSlide.vue</code></pre>
<h4>单个活跃实例的组件</h4>
<p>单个活跃实例的组件应该以 The 前缀命名,以示其唯一性<br>这不意味着组件只可用于一个单页面,而是每个页面只使用一次。这些组件永远不接受任何 prop,因为它们是为你的应用定制的,而不是它们在你的应用中的上下文。如果你发现有必要添加 prop,那就表明这实际上是一个可复用的组件,只是目前在每个页面里只使用一次。</p>
<pre><code class="javascript">**反例**
components/
|- SaleManage.vue
|- ImportExcel.vue
**正例**
components/
|- TheSaleManage.vue
|- TheImportExcel.vue</code></pre>
<h4>紧密耦合的组件名</h4>
<p>和父组件紧密耦合的子组件应该以父组件的命名为前缀.如果一个组件只在其父组件某个场景下有意义,这层关系应该体现在组件名上,因为编辑器通常按照首字母顺序组织文件.</p>
<pre><code class="javascript">**反例**
components/
|- SearchBox.vue
|- SearchItem.vue
|- SearchButton.vue
**正例**
components/
|- SearchBox.vue
|- SearchBoxItem.vue
|- SearchBoxButton.vue</code></pre>
<h4>组件命中的单词顺序</h4>
<p>组件名应该以高级别的 (通常是一般化描述的) 单词开头,以描述性的修饰词结尾。</p>
<pre><code class="javascript">**反例**
components/
|- ClearSearchButton.vue
|- ExcludeFromSearchInput.vue
|- LaunchOnStartupCheckbox.vue
|- RunSearchButton.vue
|- SearchInput.vue
|- TermsCheckbox.vue
**正例**
components/
|- SearchButtonClear.vue
|- SearchButtonRun.vue
|- SearchInputQuery.vue
|- SearchInputExcludeGlob.vue
|- SettingsCheckboxTerms.vue
|- SettingsCheckboxLaunchOnStartup.vue</code></pre>
<h4>完整单词的组件名</h4>
<p>编辑器中的自动补全已经相当友好,让书写长的组件名的代价已经可以微乎其微,同样的效率更易于理解,何乐而不为?</p>
<pre><code class="javascript">**反例**
components/
|- soManage.vue
|- woManage.vue
**正例**
components/
|- SaleOrderManage.vue
|- WorkOrderManage.vue</code></pre>
<h4>prop的大小写</h4>
<p>在声明时始终采用(camelCase),在模板和 JSX 中应该始终使用( kebab-case)。<br>单纯的遵循每个语言的约定。在 JavaScript 中更自然的是 camelCase。而在 HTML 中则是 kebab-case。</p>
<pre><code class="javascript">**反例**
props: {
'greeting-text': String
}
<WelcomeMessage greetingText="hi"/>
**正例**
props: {
greetingText: String
}
<WelcomeMessage greeting-text="hi"/>
</code></pre>
<h3>vue中变量命名规范</h3>
<p>变量命名使用主要集中在data和methods中</p>
<h4>data中更多的是名词与状态布尔类型</h4>
<p>名词:名词太多,大致分为复数、后缀加Arr、加Obj之类作为约定规则<br>状态布尔型:</p>
<p>1.表示是不是,用is+ :如 isEmpty</p>
<ol>
<li>表示有没有,用has+... : 如 hasClass</li>
<li>表示能不能,用can+... :如 canSubmit</li>
<li>单词本身的形式(过去式、进行时、将来时):had开头、ing、ed结尾等</li>
</ol>
<h4>methods中handle+以下:</h4>
<p>dd/remove,添加/移除<br>add/delete,添加/删除<br>insert/delete,插入/删除<br>start/stop,开始/停止<br>begin/end,开始/结束<br>send/receive,发送/接收<br>get/set,取出/设置<br>get/release,获取/释放<br>put/get,放入/取出<br>up/down,向上/向下<br>show/hide,显示/隐藏<br>open/close,打开/关闭<br>increment/decrement,增加/减少<br>lock/unlock,锁/解锁<br>next/previous,下一个/前一个<br>create/destroy,创建/销毁<br>min/max,最小/最大<br>visible/invisible,可见/不可见<br>pop/push,出栈/入栈<br>store/query,存储/查询</p>
<p>结合业务:<br>表单提交:submit、send<br>表单增删改查:add、delete、update、search、reset<br>上传附件:upload<br>关闭打开弹窗:open/close<br>检查:check</p>
<h3>参考链接</h3>
<p><a href="https://link.segmentfault.com/?enc=CbqMDwiXuGGihtct1Z2AQw%3D%3D.RBK7Yucb%2FrmbA0i%2BTWRBFUHzcEd6FDNUBEurPKi63BONZGsquqdpcdVf26m70Pn9" rel="nofollow">CSS命名方式=》BEM</a><br><a href="https://link.segmentfault.com/?enc=V8xHgaKPLIzASFSHweDZwA%3D%3D.IywIiAsIqCabXGb%2FekkU1qE6uEeS%2B2fcmHgZomx98Fe9RxtUGkuLb%2FOO%2F64HjZX2sFu3Ckq3TcXwrKu8KjVcj9%2F8OQn3W%2Bj0uy44spqm8N4%3D" rel="nofollow">如何定义一个好的变量名</a><br><a href="https://link.segmentfault.com/?enc=%2FPhrmnVIXglRlr%2Fw9qkVGg%3D%3D.k%2F%2F68h2A93l56pn0qRp81BG4KsRFztv3nFnk6bUp5f280xRp74B4k7NHwM9M4U3h" rel="nofollow">理解CSS命名规范--BEM</a><br><a href="https://link.segmentfault.com/?enc=asd2L7WHLsowUGI02ngtNQ%3D%3D.1rEqlyLvwg102wNPZqBx9RL8477LrybR3L0la2gTCNMVqzxhn4EDbd9S33%2FJXv%2FoZSRIS6%2B5tuY8%2FeppTh1mcQ%3D%3D" rel="nofollow">聊聊 Vue 组件命名那些事</a><br><a href="https://link.segmentfault.com/?enc=aJhCrXzEL1j7giB2ySaIpw%3D%3D.Cmi1JtL89KAJefLt5OyPeij9eXbnPQimVqADuCzWg0HtGg%2FWEbqDcPtZvPcYNiJu" rel="nofollow">谈谈函数的命名规范</a><br><a href="https://link.segmentfault.com/?enc=y3bdJ5jI7HtBkSEShiM0Cg%3D%3D.QeIeE4cRfhTQy4zLaUwkkW%2FMfQQ%2FHyFxRRpVTfjA702ql2BLHuV43tUGc0VkFMFS" rel="nofollow">vue组件命名指南,不为取名而纠结</a></p>
原生js实现拾色器插件
https://segmentfault.com/a/1190000013653408
2018-03-11T12:49:25+08:00
2018-03-11T12:49:25+08:00
我是南方大汉
https://segmentfault.com/u/woshinanfangdahan
3
<h2>原生js实现拾色器插件</h2>
<h3>前言</h3>
<p>插件功能只满足我司业务需求,如果希望有更多功能的,可在下方留言,我尽量扩展!如果你有需要或者喜欢的话,可以给我github来个star ?</p>
<blockquote>
<a href="https://link.segmentfault.com/?enc=kaUMyF8ZjsOl0phK9aDnWQ%3D%3D.wkSbGaJ1Qr0Kp47FDFaTY3v98qNyQMgV%2F5o35FFvgcoMFc63kpypcOZlSvGxJSdOSqTFkLMNt5UpghcKcz47MQ%3D%3D" rel="nofollow">仓库地址</a><p><a href="https://link.segmentfault.com/?enc=5DTo8S0rxvFamkKa1b0Uag%3D%3D.%2ByshtINbuVkHv9ClAlm%2BqjWnGmdPn%2Bc4QYs2mAIt82%2F0EnhUSBH2kEYsXOLvyIikJe93HuJGuOKgzsyD4XnVYg%3D%3D" rel="nofollow">在线预览</a></p>
</blockquote>
<h4>预览</h4>
<p><img src="/img/remote/1460000013653413?w=421&h=500" alt="拾色器" title="拾色器"></p>
<h4>准备</h4>
<p>首先在页面中引入js文件</p>
<p>在页面中写上如下代码:</p>
<pre><code class="javaScript">Colorpicker.create({
bindClass:'picker', // 这里的picker可随意填 不需要跟我一样
change: function(elem,hex){
// elem: 绑定的元素
// hex:当前选中颜色的hex值
elem.style.backgroundColor = hex;
}
})</code></pre>
<blockquote>
<strong><code>bindClass</code></strong>:参数填入你要绑定拾色器的元素,页面中class为picker有几个,拾色器将会生成几个。拾色器将会分别绑定每个元素。点击每个元素时,都会自动打开该元素绑定的拾色器。<p><strong><code>change</code></strong>:在选择的色彩改变的时候会触发该回调方法。会回传两个参数,第一个<code>elem</code>就是该拾色器生成时绑定的<code>picker</code>;第二个参数,hex代表是回传的颜色值。起初是插件直接改变绑定元素的颜色,但是想到有些拾色器插件是绑定input表单,改变表单颜色值,有些是改变绑定元素的颜色。所以为了让使用者自由度更高点,暂提供两个回调参数让你自定义。如上面 我是直接改变元素颜色。</p>
<p><strong>如果需要更多回调方法,我会尽量扩展</strong></p>
</blockquote>
<h3>结尾</h3>
<p>如有什么功能需要增加的,可在评论区留言,我尽量满足。如有什么疏忽或错误,希望您指出。我会尽早修改,以免误导他人。</p>
原生js实现移动端选择器插件
https://segmentfault.com/a/1190000013366588
2018-02-25T04:07:16+08:00
2018-02-25T04:07:16+08:00
我是南方大汉
https://segmentfault.com/u/woshinanfangdahan
10
<h2>原生js实现移动端选择器插件</h2>
<h3>前言</h3>
<p>插件功能只满足我司业务需求,如果希望有更多功能的,可在下方留言,我尽量扩展!如果你有需要或者喜欢的话,可以给我github来个star ?</p>
<blockquote>
<a href="https://link.segmentfault.com/?enc=Z91K%2FiwoqSbvHmTYs7hGHw%3D%3D.3r0GiH2KMwEvyJnVdTb0hHNbbB0RhBTtkGQpuGjSYIS%2BSqDf%2BrTxNyb0mqZYuhksoRtX4%2FELaILqr9YkRjurEg%3D%3D" rel="nofollow">仓库地址</a><p><a href="https://link.segmentfault.com/?enc=7CJZ1m3vfygiV0N4gr1yoQ%3D%3D.lWl8VwsZposLv%2BbIo3LtHfaEMf32Dg3avpGj%2BOPRwQImYnzQJgH40lG5OPtiFYXz6BGCoFZTbb1ajtBl940Hag%3D%3D" rel="nofollow">在线预览(记得将浏览器切换到手机模式)</a></p>
</blockquote>
<h4>预览</h4>
<p><img src="/img/remote/1460000013373342?w=284&h=500" alt="省市区" title="省市区"></p>
<h4>准备</h4>
<p>首先在页面中引入css,js文件</p>
<p>每次需要弹出该组件时通过new一个实例来生成,代码如下:</p>
<pre><code class="javaScript">var data = {
1:{
2:[3,4]
}
}
var pickerView = new PickerView({
bindElem: elem, // 绑定的元素,用于区别多个组件存在时回显区别,如果单个可以随意填某个元素
data: data, // 说明:该参数必须符合json格式 且最里层是个数组,如上面的data变量所展示的[3,4]。
title: '标题2', // 顶部标题文本 默认为“标题”
leftText: '取消', // 头部左侧按钮文本 默认为‘取消’
rightText: '确定', // 头部右侧按钮文本 默认为“确定”
rightFn: function( selectArr ){ // 点击头部右侧按钮的回调函数,参数为一个数组,数组对应滚轮中每项对应的值
}
});</code></pre>
<blockquote>字段介绍如上注释,滚轮的数量取决于你json嵌套的层数。如下:</blockquote>
<pre><code class="javascript">var data = [1,2,3]</code></pre>
<p><img src="/img/remote/1460000013366594?w=281&h=202" alt="data1" title="data1"></p>
<pre><code class="javaScript">var data = {
"小明家":["小明爸爸","小明妈妈","小明爷爷","小明奶奶","小明爸爸","小明妈妈","小明爷爷","小明奶奶"],
"小红家":["小红爸爸","小红妈妈"]
}</code></pre>
<p><img src="/img/remote/1460000013366595?w=280&h=201" alt="data2" title="data2"></p>
<h4>案例</h4>
<pre><code class="html"><!-- html -->
<button style="font-size:50px;" id="btn">按钮</button>
<div class="showText"></div></code></pre>
<blockquote>button标签是用来每次点击的时候打开组件<p>div标签用来展示选择的内容</p>
</blockquote>
<pre><code class="javaScript">//js
// var data = 地级市json数据,过大 就不展示了
var data = {
"小明家":{
"小明爸爸":[1,2,6,7,7,8,8,9,0,6,98,76,5],
"小明妈妈":[3,4]
},
"小红家":{
"小红爸爸":[5,6],
"小红妈妈":[7,8]
}
}
var btn = document.getElementById("btn");
btn.onclick = function(){
var pickerView = new PickerView({
bindElem: btn,
data: data,
title: '家庭',
leftText: '取消',
rightText: '确定',
rightFn: function( selectArr ){
console.log(selectArr,'selectarr');
// 将家庭成员展示到showText类名的div中
document.querySelector(".showText").innerText = selectArr.join("-");
}
});
}</code></pre>
<blockquote>说明: 每次显示组件的时候都需要new一个实例,如上button标签每次被点击的时候都new一个。效果如下:</blockquote>
<p><img src="/img/remote/1460000013366596?w=281&h=500" alt="预览" title="预览"></p>
<h3>结尾</h3>
<p>如有什么功能需要增加的,可在评论区留言,我尽量满足。如有什么疏忽或错误,希望您指出。我会尽早修改,以免误导他人。</p>
原生js实现拖拽缩放预览图片插件
https://segmentfault.com/a/1190000013280033
2018-02-13T02:12:15+08:00
2018-02-13T02:12:15+08:00
我是南方大汉
https://segmentfault.com/u/woshinanfangdahan
0
<h2>原生js实现拖拽缩放预览图片插件</h2>
<h3>前言</h3>
<p>插件功能暂只满足我司业务需求,如果希望有更多的功能,可在下方留言,我尽量扩展!如果你有需要或者喜欢的话,可以给我github来个star ?</p>
<blockquote>
<a href="https://link.segmentfault.com/?enc=2rSppa4txxtFRZtiyfCZCg%3D%3D.3OkbwM4dm0Fe0g1J0c9r%2BEW0G5dLHlAx91Ytl2%2FlsTa9MPGXE83Nxf1LecVrsb25eOuCIcFwClih3H77D1Q57Q%3D%3D" rel="nofollow">仓库地址</a><br><a href="https://link.segmentfault.com/?enc=alP%2FnvBVqQv8B4ITE87UDA%3D%3D.M3%2B65sqM77F51sh1KGoZnYRmcMgk1310%2FMVPvqLFMjTU3uU8MgMnfdYyHY2Dv1BsrDE0tuPOAB3W21Vb0V6Avw%3D%3D" rel="nofollow">在线预览</a>
</blockquote>
<h3>准备</h3>
<ol>
<li>引入preview.js文件</li>
<li>
<p>指定一个容器的id,插件只预览该容器内的图片,举个栗子?:</p>
<pre><code class="html"><div id="wrap">
<div>
<img src="./data/girl1.jpg" alt="" />
</div>
<img src="./data/girl2.jpg" alt="" />
<img src="./data/girl3.jpg" alt="">
</div></code></pre>
<blockquote>其中id为wrap的div就是 2 中所指的容器。插件只预览该容器下的所有图片。</blockquote>
<pre><code class="javaScript">var preview = new Preview({
imgWrap: 'wrap' // 指定该容器里的图片点击预览
})</code></pre>
<blockquote>imgWrap键的值就是容器的id</blockquote>
</li>
<li>如果觉得样式不满意什么的,可以直接css覆盖就可以了。</li>
</ol>
<h3>预览</h3>
<p><img src="/img/remote/1460000013280038" alt="girl" title="girl"></p>
<h3>总结</h3>
<p>如有疏忽或错误,希望您及时指出,我会尽早修改?。有什么需要交流的可在评论区与我交流</p>
原生js实现省市区三级联动插件
https://segmentfault.com/a/1190000013267771
2018-02-12T00:31:00+08:00
2018-02-12T00:31:00+08:00
我是南方大汉
https://segmentfault.com/u/woshinanfangdahan
5
<h2>原生js实现省市区三级联动插件</h2>
<h3>前言</h3>
<p>插件功能只满足我司业务需求,如果希望有更多功能的,可在下方留言,我尽量扩展!如果你有需要或者喜欢的话,可以给我github来个star ?</p>
<blockquote>
<a href="https://link.segmentfault.com/?enc=%2BH8nvHK1%2BAPLrbMb%2F%2FQ4VQ%3D%3D.FKNHYHNIt0W2thVhqxA2wUKG0GotshdKv7OaxESzf8QsnpBY2gLLHHzalY6q942pUU3j5071x5kPyQkfJynhTQ%3D%3D" rel="nofollow">仓库地址</a><p><a href="https://link.segmentfault.com/?enc=DUxH00WgvQj4Ts6Iixo6Xw%3D%3D.ziBExJ3vTMqQLn%2BI2rJh8DLwTOYBCTqq0cb7wZjcuGFZHBPATMbwnPW%2BQ6wF74jA83Zhbd4gmNdJ1XKatUm50g%3D%3D" rel="nofollow">在线预览</a></p>
</blockquote>
<h4>准备</h4>
<pre><code class="html">// 页面上先引入css与js文件
<div id="wrap"></div></code></pre>
<p>页面中的容器标签不限制,只需给个id就行</p>
<pre><code class="javaScript">var address = new Address({
wrapId: 'wrap',
showArr: ['provinces','citys','areas'],
beforeCreat:function(){
console.log("beforeCreat")
},
afterCreat:function(){
console.log('afterCreat');
}
})</code></pre>
<ul>
<li><code>wrapId:"wrap" // 此处的wrap就是上面容器的id </code></li>
<li>
<p><code>showArr: ['provinces','citys','areas'] // 此处分别代表省、市、区容器的id </code></p>
<blockquote>举个例子:如果传递的数组<code>['provinces','citys','areas']</code><strong>长度为3</strong>,那么将会出现省市区,数组中三个字符串分别是省、市、区容器的id<p><img src="/img/remote/1460000013267776?w=674&h=112" alt="省市区" title="省市区"></p>
<p>如传递的数组<code>['provinces','citys']</code><strong>长度为2</strong>,那么将会出现省市,数组中的两个字符串分别是省、市容器的id</p>
<p><img src="/img/remote/1460000013267777?w=454&h=122" alt="省市" title="省市"></p>
<p>如数组长度为1的时候就不说了</p>
</blockquote>
</li>
<li>
<code>beforeCreat</code> 插件开始创建前执行的回调函数</li>
<li>
<p><code>afterCreat</code> 插件创建完成后执行的回调函数</p>
<blockquote><img src="/img/remote/1460000013267778?w=264&h=200" alt="console" title="console"></blockquote>
</li>
</ul>
<h4>预览</h4>
<p><img src="/img/remote/1460000013267779?w=678&h=438" alt="省市区" title="省市区"></p>
<h3>总结</h3>
<p><strong>如有什么功能需要增加的,可在评论区留言,我尽量满足。如有什么疏忽或错误,希望您指出。我会尽早修改,以免误导他人。</strong></p>
CSS水平垂直居中解决方案
https://segmentfault.com/a/1190000013249094
2018-02-10T13:34:51+08:00
2018-02-10T13:34:51+08:00
我是南方大汉
https://segmentfault.com/u/woshinanfangdahan
7
<h2>CSS水平垂直居中解决方案</h2>
<h3>准备</h3>
<p>创建元素</p>
<pre><code class="html"><div class="parent">
<div class="child">child</div>
</div></code></pre>
<h3>垂直水平居中方案一:知道宽度的情况下 absolute+margin负值</h3>
<pre><code class="html">.parent {
width:400px;
height:400px;
background: red;
position: relative;
}
.child {
position: absolute;
left:50%;
top:50%;
background: yellow;
width:50px;
height:50px;
margin-left:-25px;
margin-top:-25px;
}</code></pre>
<h3>垂直水平居中方案二:不知道宽高的情况下 absolute+transform</h3>
<pre><code class="html">.parent {
width:400px;
height:400px;
background: red;
position: relative;
}
.child {
position: absolute;
left:50%;
top:50%;
transform: translate(-50%,-50%);
}</code></pre>
<h3>垂直居中方案三:position+margin:auto</h3>
<pre><code class="html">.parent {
position:relative;
width:200px;
height:200px;
background: red;
}
.child {
width:80px;
height:40px;
background: yellow;
position: absolute;
left:0;
top:0;
right:0 ;
bottom:0;
margin:auto;
}</code></pre>
<h3>垂直居中方案四:+ 多行文本的垂直居中 :table-cell+vertical-align:middle;</h3>
<pre><code class="html">.parent {
height: 300px;
width:400px;
border: 1px solid red;
display: table-cell;
vertical-align: middle;
text-align: center;
}
.child {
display: inline-block;
width:50px;
height:50px;
background: blue;
}
/* 或者 */
.parent {
width: 400px;
height: 300px;
display: table-cell;
vertical-align: middle;
border: 1px solid red;
text-align: center;
}
.child {
display: inline-block;
vertical-align: middle;
background: blue;
}</code></pre>
<h3>垂直居中方案五:display: flex</h3>
<pre><code class="html">.parent {
width:400px;
height:200px;
background:red;
display: flex;
justify-content:center;
align-items:center;
}
.child {
height:100px;
width:100px;
background:green;
}</code></pre>
<h3>垂直居中方案六:伪元素</h3>
<pre><code class="html">.parent {
width:200px;
height:200px;
background:red;
text-align: center;
}
.child {
height:100px;
width:100px;
background:yellow;
display: inline-block;
vertical-align: middle;
}
.parent:before {
content:"";
height:100%;
vertical-align: middle;
display: inline-block;
}</code></pre>
<blockquote>在下的文章都是学习过程中的总结,如果发现错误,欢迎留言指出~</blockquote>
webpack入门
https://segmentfault.com/a/1190000013075997
2018-02-01T00:21:18+08:00
2018-02-01T00:21:18+08:00
我是南方大汉
https://segmentfault.com/u/woshinanfangdahan
0
<h2>前言</h2>
<p>本篇是我经过一周多的时间阅读博客及官方文档所写的文章,算是自己对webpack学习的总结。有篇个人认为非常不错的webpack文章贴在最下面的<strong>参考链接</strong>的第一条。本篇介绍的是用webpack自己搭一个react应用</p>
<h3>一、概念</h3>
<ul>
<li>
<strong>入口(entry)</strong><br>入口起点(entry point)指示 webpack 应该使用哪个模块,来作为构建其内部依赖图的开始。进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。</li>
<li>
<strong>输出(output)</strong><br>output 属性告诉 webpack 在哪里输出它所创建的 bundles,以及如何命名这些文件。</li>
<li>
<strong>loader</strong><br>loader 让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)。loader 可以将所有类型的文件转换为 webpack 能够处理的有效模块,然后你就可以利用 webpack 的打包能力,对它们进行处理。</li>
<li>
<strong>插件(plugins)</strong><br>用于执行范围更广的任务。插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量。插件接口功能极其强大,可以用来处理各种各样的任务。</li>
</ul>
<h4>准备</h4>
<pre><code>`npm init` 自动帮你创建package.json文件
`npm install --save-dev webpack // 安装Webpack`
新建文件夹app、public文件分别用于存放源文件及打包后的存放文件。在app文件夹中创建index.js与index.html文件。再创建一个webpack的配置文件webpack.config.js。使用前记得安装下webpack `npm install webpack -g`,当然你也可以不全局安装。</code></pre>
<pre><code class="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>webpack</title>
</head>
<body>
<script src="../public/bundle.js"></script>
</body>
</html></code></pre>
<h4>1.入口</h4>
<pre><code class="javascript">const config = {
entry: './app/index.js'
};
module.exports = config;</code></pre>
<p>entry就是指示 webpack 应该使用哪个模块,来作为构建其内部依赖图的开始。进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。本项目中的入口文件就是app文件夹中的index.js文件。(多入口及更多选项参考官方文档)</p>
<h4>2.出口</h4>
<pre><code class="javascript">var config = {
entry: "./app/index.js", // 入口文件
output:{
filename:"bundle.js", //filename 用于输出文件的文件名。
path:path.resolve(__dirname, './public'), //目标输出目录 path 的绝对路径。__dirname
publicPath:path.resolve(__dirname, './public') //publicPath取决于你的网站根目录的位置,因为打包的文件都在网站根目录了,这些文件的引用都是基于该目录的。假设网站根目录为public,引用的图片路径是’./img.png’,如果publicPath为’/’,图片显示不了,因为图片都打包放在了dist中,那么你就要把publicPath设置为”/dist”
}
}
module.exports = config;</code></pre>
<p>填好入口与出口配置后、在命令行中输入<code>.node_modules/.bin/webpack --config webpack.config.js </code>如果你是全局安装webpack的话可直接输入<code>webpack --config webpack.config.js</code>,直接输入<code>webpack</code>也可以,他会识别当前目录中的webpack.config.js。然后你就可以再public文件夹中看到名为bundle.js文件生成了。</p>
<h4>3.更快捷的执行打包任务</h4>
<p>如果你不想每次在命令行中输入那么长的代码,那就在package.json中的scripts字段中配置如下</p>
<pre><code class="javascript">"scripts": {
"build": "webpack --config webpack.config.js"
}</code></pre>
<p>不管你是否全局安装的webpack在scripts中他会自动识别node_modules中的webpack,所以不需要在里面输入<code>./node_modules...</code>。现在你可以再命令行中输入<code>npm run build</code>进行打包了。</p>
<h4>4.webpack-dev-server</h4>
<p><code>webpack-dev-server</code> 是一个小型的Node.js Express服务器,<strong>webpack-dev-server默认会以当前目录为基本目录</strong>,除非你制定它.通过webpack-dev-server就可实现热加载了。使用前需要用npm安装<code>npm install webpack-dev-server --save-dev</code>,它根据webpack.config.js文件中的选项构建。常见的选项如下:</p>
<table>
<thead><tr>
<th>webpack-dev-server选项</th>
<th>选项说明</th>
</tr></thead>
<tbody>
<tr>
<td>content-Base</td>
<td>默认情况下,webpack-dev-server会从项目的根目录提供文件,可以通过此选项设置文件的目录名</td>
</tr>
<tr>
<td>port</td>
<td>服务器使用的端口,默认情况下为8080</td>
</tr>
<tr>
<td>inline</td>
<td>设为true时可以在文件发生变化时,更新页面</td>
</tr>
<tr>
<td>colors</td>
<td>设置终端输出字体颜色</td>
</tr>
<tr>
<td>historyApiFallback</td>
<td>当设置为true时,访问所有服务器上不存在的文件,都会被重定向到/,也就是index.html文件</td>
</tr>
</tbody>
</table>
<p>代码如下:</p>
<pre><code class="javascript">var config = {
entry: "./app/index.js", // 入口文件
output: {
filename: "bundle.js", //filename 用于输出文件的文件名。
path: path.resolve(__dirname, './public') //目标输出目录 path 的绝对路径。__dirname
},
devServer: {
contentBase: "./public",//本地服务器所加载的页面所在的目录
historyApiFallback: true,//不跳转
inline: true,//实时刷新(为什么这个我不加都能热更新???)
port: 8888, // 想webpack-dev-server在端口8888启动
}
}</code></pre>
<p><strong>webpack-dev-server默认会以当前目录为基本目录</strong>,因为我指定到了public,所以在public中新建个index.html,并在html中引入当前文件夹中的bundle.js。</p>
<p>在package.json 添加如下代码就可快捷启动:</p>
<pre><code class="javascript">"scripts": {
"build": "webpack --config webpack.config.js",
"server": "webpack-dev-server --config webpack.config.js"
}</code></pre>
<p>然后再命令行中输入<code>npm run server</code>启动webpack-dev-server。并在浏览器打开<a href="https://link.segmentfault.com/?enc=V%2BZcOucgNvJRkHpG1tWe8Q%3D%3D.VPYgUh1V3LDbuwM27Svp5VMQQWN9oCMiIAJ9bDlXKoA%3D" rel="nofollow">http://localhost</a>:8888/就可以查看了。修改了index.js中的代码,也会实时刷新了。</p>
<h4>5 babel</h4>
<p>loader 用于对模块的源代码进行转换。loader 可以使你在 import 或"加载"模块时预处理文件。因此,loader 类似于其他构建工具中“任务(task)”,并提供了处理前端构建步骤的强大方法。loader 可以将文件从不同的语言(如 TypeScript)转换为 JavaScript,或将内联图像转换为 data URL。loader 甚至允许你直接在 JavaScript 模块中 import CSS文件!</p>
<p>Loaders需要单独安装并且需要在webpack.config.js中的modules关键字下进行配置,Loaders的配置包括以下几方面:</p>
<ul>
<li>test:一个用以匹配loaders所处理文件的拓展名的正则表达式(必须)</li>
<li>loader:loader的名称(必须)</li>
<li>include/exclude:手动添加必须处理的文件(文件夹)或屏蔽不需要处理的文件(文件夹)(可选);</li>
<li>query:为loaders提供额外的设置选项(可选)</li>
</ul>
<h5>css</h5>
<p>webpack提供两个工具处理样式表,css-loader 和 style-loader,二者处理的任务不同,css-loader使你能够使用类似@import 和 url(...)的方法实现 require()的功能,style-loader将所有的计算后的样式加入页面中,二者组合在一起使你能够把样式表嵌入webpack打包后的JS文件中。</p>
<p><code>npm install --save-dev style-loader css-loader //安装</code><br>配置如下:</p>
<pre><code class="javascript">module: {
rules: [
{
test: /\.css$/,
use: [
{ loader: 'style-loader' },
{ loader: 'css-loader' }
]
}
]
}</code></pre>
<p>可以认为他是从后面开始往前面解析 css-loader ==> style-loader,所以如果是less文件除了安装less-loader之外配置文件应该写在css-loader后面,如下:</p>
<pre><code class="javascript">test: /\.less$/,
use: [
{ loader: 'style-loader' },
{ loader: 'css-loader' },
{ loader: 'less-loader' }
]</code></pre>
<h6>这样我们平时最基本的最简单的配置大致完成了</h6>
<h4>6.babel</h4>
<p>Babel 是一个 JavaScript 编译器,把用最新标准编写的 JavaScript 代码向下编译成可以在今天随处可用的版本。 这一过程叫做“源码到源码”编译, 也被称为转换编译。其中核心的功能可以在babel-core模块中获得。其他的部分根据用户的需求来下载:如果想与webpack一起使用,需要安装babel-loader模块;如果想使用ES6特性,需要安装babel-preset-env;如果想使用React JSX,那么需要安装babel-preset-react。当然还有一些其他的配置模块,这里并没有全部列出。接下来我们看一个简单的React例子:</p>
<blockquote>安装babel相关的:<code>npm install --save-dev babel-core babel-loader babel-preset-env babel-preset-react</code><br>安装react相关的:<code>npm install react -react-dom --save</code>
</blockquote>
<p>webpack.config.js中加入如下代码:</p>
<pre><code class="javascript">{
test: /(\.jsx|\.js)$/,
use: {
loader: "babel-loader",
options: {
presets: [
"env", "react"
]
}
},
exclude: /node_modules/
}</code></pre>
<p>就能实现简易的react应用了。到目前为止public中的index.html文件都是自己写在public文件夹中的,引入js文件都是自己手动的,所以再来介绍下插件</p>
<h4>7.plugin</h4>
<p>loader 被用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量。插件接口功能极其强大,可以用来处理各种各样的任务。</p>
<p><strong>HtmlWebpackPlugin</strong>这个插件的作用是依据一个简单的index.html模板,生成一个自动引用你打包后的JS文件的新index.html。这在每次生成的js文件名称不同时非常有用(比如添加了hash值)。在编译过程中,插件会依据此模板生成最终的html页面,会自动添加所依赖的 css, js,favicon等文件</p>
<blockquote>安装<code>npm install --save-dev html-webpack-plugin</code>
</blockquote>
<p>webpack.config.js顶部引入<code>var HtmlWebpackPlugin = require('html-webpack-plugin');</code><br>在config中的plugins字段中添加如下代码:</p>
<pre><code class="javascript">plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname , './app/index.html') // 引用app文件夹中的index.html作为模板
})
]
</code></pre>
<p>index.html模板如下:</p>
<pre><code class="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>
<div id="root"></div>
</body>
</html></code></pre>
<p>在终端中输入<code>npm run build</code>,接下来你将会在public文件夹中看到自动生成的index.html文件了。</p>
<p><strong>模块热替换(Hot Module Replacement)</strong><br>模块热替换(HMR - Hot Module Replacement)功能会在应用程序运行过程中替换、添加或删除模块,而无需重新加载整个页面。主要是通过以下几种方式,来显著加快开发速度:</p>
<ul>
<li>保留在完全重新加载页面时丢失的应用程序状态。</li>
<li>只更新变更内容,以节省宝贵的开发时间。</li>
<li>调整样式更加快速 - 几乎相当于在浏览器调试器中更改样式。</li>
</ul>
<p>之前的刷新是页面全局刷新,如果我们只想局部刷新即只刷新修改的部分,需要使用webpack的HotModuleReplacementPlugin插件,在devServer中添加hot:true参数,在webpack.config.js的plugins中添加下面的信息:</p>
<blockquote><code>new webpack.HotModuleReplacementPlugin()</code></blockquote>
<h3>参考链接</h3>
<blockquote>
<a href="https://segmentfault.com/a/1190000006178770#articleHeader6">入门 Webpack,看这篇就够了</a><br><a href="https://link.segmentfault.com/?enc=HKXqNw2%2FQ4RWCOGsZOqcIQ%3D%3D.QfbnS2nlPaqa5mwPCHF00%2F7%2FjbXn%2Bnj6AeSXvm2xGBh6qcxeNothabIz6VIKMlQYgO%2BtsIfPfKo8NEg8Jj%2BGAA%3D%3D" rel="nofollow">博客园xfshen的webpack</a><br><a href="https://link.segmentfault.com/?enc=OjXeC7OHr80oS6QE0cXszw%3D%3D.UiKnaPbqQyjq8t1PdeSviG3gcBdo%2BRvxSOX89Qf9RQeed00A6yLBL1hxa2fI83TH" rel="nofollow">webpack中文文档</a>
</blockquote>
<hr>
<p>在下的文章都是学习过程中的总结,如果发现错误,欢迎留言指出~</p>
基于react、socket.io、node.js仿微信开发
https://segmentfault.com/a/1190000012925099
2018-01-21T21:39:27+08:00
2018-01-21T21:39:27+08:00
我是南方大汉
https://segmentfault.com/u/woshinanfangdahan
15
<h2>前言</h2>
<p>这个项目是我自学react+redux的第一个项目,并结合自己之前所学的node+mongodb,来模仿开发微信客户端。利用每天下班时间边学习边写。由于本人技术水平有限,比较适合新手。目前还没有写完。喜欢的话可以帮忙给我github点个star ^_^</p>
<h3>项目地址</h3>
<p><a href="https://link.segmentfault.com/?enc=0vC4DBOuJ696P8rgcgmd%2BA%3D%3D.SZiiul13pSVDWpIylekz8VC2DS4eNCEOCRE41OOymQtemAeGG4mbqTpugfQXejMN" rel="nofollow">https://github.com/zhouatie/w...</a></p>
<h3>技术栈</h3>
<p>react+redux+react-router4+socket.io+axios+node.js+mongodb</p>
<h3>说明</h3>
<pre><code>本地启动mongodb服务
分别进入wechat跟server文件夹npm install
wechat里npm run start
server里node app.js 和 chat.js 这两个文件</code></pre>
<blockquote>开发环境:macbook pro 、vscode、Chrome、node<p>如果npm install太慢导致有些npm依赖包下载失败 你可以看控制台的报错信息,再手动npm install 具体的开发包,推荐使用淘宝的注册源,直接运行</p>
</blockquote>
<p><code>npm install -g cnpm --registry=https://registry.npm.taobao.org</code></p>
<h3>目标功能</h3>
<ul>
<li>[√] 注册</li>
<li>[√] 登录</li>
<li>[√] 添加好友</li>
<li>[√] 支持私聊</li>
<li>[√] 消息列表的展示</li>
<li>[√] 未读消息数量的显示</li>
<li>[√] axios数据跨域的设置</li>
<li>[ ] 群聊</li>
<li>[√] 上传头像</li>
<li>[√] 个人信息的编辑</li>
<li>[ ] 朋友圈</li>
</ul>
<h3>部分截图</h3>
<p><img src="/img/remote/1460000012925104" alt="私聊" title="私聊"></p>
<p><img src="/img/remote/1460000012925105" alt="上传头像" title="上传头像"></p>
<h3>总结</h3>
<p>1.之前写vue项目的时候,在main.js文件中写express接口,就行了,就不存在跨域问题。在create-react-app启动的静态资源服务中,实在找不到哪里可以写接口,找了好久的node_modules ,都不知道在哪里下手。好在create-react-app中的package.json中加上:<code>proxy:http://localhost:4000</code>就能解决跨域问题了。</p>
<p>2.在app.js页面中,使用的是express框架,写socket.io不知道为什么会提醒跨域问题,而我前面的登录接口用axios跨域就没有问题,而且我在express的头部做了CORS处理,还是存在跨域问题。所以只能另启了一个node服务,采用原生node.js编写,跨域就成功了。但是我在新写的服务中,换成用express框架,结果也提示了存在跨域问题。目前个人猜测express可能有什么跨域机制。</p>
<p>3.在引入react-router4的时候遇到了很多疑难杂症,晚上大部分的react-router4一下的版本。按照网上来做,好多报错,到处找博客找文章。后来通过react-router英文文档的阅读解决了各种报错问题。</p>
<p>4.我是通过redux来更新消息列表,中间出现store数据更新了,组件却不渲染。后来求助好友后,原来是我强制修改了state导致页面无法即使刷新。</p>
<p>5.formdata上传文件,相当于表单上传,头部为<code>Content-Type:multipart/form-data</code>,这点要注意了!</p>
<blockquote>注意: Multer 不会处理任何非 multipart/form-data 类型的表单数据。具体见 <a href="https://link.segmentfault.com/?enc=P8WzQZ9bl6serLw9y9ayJQ%3D%3D.kRm6HIDJEiU3Y9XWzKhRdklNZh63fxn8hZcugUWnEztFkWJax9s5kAtp8UT4KXP3" rel="nofollow">multer</a>
</blockquote>
<pre><code class="javascript">var multer = require('multer');
var upload = multer({ dest: '../wechat/public/logos' }); // dest 指的是图片存到哪个文件夹里
// 上传头像
app.post("/uploadLogo", upload.single("avatar"), (req, res) => {
User.update({ _id: req.body.id }, { $set: { logo: './logos/' + req.file.filename } }, function () {
res.send({
status: "success",
url: './logos/' + req.file.filename
})
})
})</code></pre>
<h3>参考资料</h3>
<p>《深入浅出React和Redux》-- 程墨</p>
<p>《MongoDB实战(第二版)》</p>
<p><a href="https://link.segmentfault.com/?enc=whAJMdX6exxqI29BU79RNg%3D%3D.3Kg3xRFkE353W0EAwoPrnGB3iBVJ%2F%2FoFm5OiphwmF%2FIlsmhZ7ZajrxFCBzkrh%2F70k0P2vFEbiLyB0bob2ZUGdw%3D%3D" rel="nofollow">react-router</a></p>
<p><a href="https://link.segmentfault.com/?enc=N2fLbwzFCk1UetMq5%2B8gWg%3D%3D.bbpbWvfHFhrT8kA9uD4jUBKY7sDt7%2Bd59iooqi9hWxzZkwAkZvA32ViE%2BH6wawAD" rel="nofollow">react</a></p>
<p><a href="https://link.segmentfault.com/?enc=QgEFPCkXZ53gXaIumI%2BDIg%3D%3D.ibJjSdLjeDARjw%2BLJiz8dWhW4n%2B47zmVS7M5wWE71CKiqCtwz9i6JSm%2Fg10m2gfJ" rel="nofollow">redux中文文档</a></p>
<p><a href="https://link.segmentfault.com/?enc=bTte6h7Yg7707eoQGQ3p7w%3D%3D.RW3RS13NbPnQyUzanqUdWa4%2BT27SQMyhWi74NZtvBmllvdDnKaIPJepQN8NF6V8qJM6f3NjemgBZdpGuKE7wiQ%3D%3D" rel="nofollow">mongoose</a></p>
<p><a href="https://link.segmentfault.com/?enc=hCAo%2F17MYPjs2KgA6gCoQQ%3D%3D.IL9XfUKqEzZWRF88Ha263LBalQVO08EMX5lx%2F119z4T%2BCjhr4sbjV0uA%2Frh4Lv3tQNw0H1%2ByhIYf%2BWLU4rSFog%3D%3D" rel="nofollow">基于 Vue、Nodejs、Socket.io 的聊天应用</a></p>
<p><a href="https://link.segmentfault.com/?enc=UJ9xdmzfRmqT4YKtSSPkhQ%3D%3D.V80ZHP9uA9LicGnDlrUx9bXLc3Qr%2BgGgjDJ08vt8G9aW%2BHYghTxCCzA9UnqHhzhR" rel="nofollow">multer</a></p>
<blockquote>文章都是学习过程中的总结,如果发现错误,欢迎留言指出</blockquote>
node.js实现formdata上传文件
https://segmentfault.com/a/1190000012918178
2018-01-20T19:10:32+08:00
2018-01-20T19:10:32+08:00
我是南方大汉
https://segmentfault.com/u/woshinanfangdahan
4
<h2>node.js实现formdata上传文件</h2>
<h4>1.关于formdata</h4>
<blockquote>XMLHttpRequest Level 2 添加了一个新的接口——FormData。利用 FormData 对象,我们可以通过 JavaScript 用一些键值对来模拟一系列表单控件,我们还可以使用 XMLHttpRequest 的 send() 方法来异步的提交表单。与普通的 Ajax 相比,使用 FormData 的最大优点就是我们可以异步上传二进制文件。<br><a href="https://link.segmentfault.com/?enc=zq9OqkHtRg5ZUuMLmbeqlg%3D%3D.cc0r%2BvORy28l53rtwy8vBWbbearZ%2FnF%2B6P2y6fe1iQ8fVt4x2bnkXBHC%2BmS%2FxoLL3xqQtPZd0oG3%2BUf34heQ5A%3D%3D" rel="nofollow">FormData的api</a>
</blockquote>
<h6>方法一:</h6>
<p>创建一个空FormData对象:<br><code>var formData = new FormData()</code><br>使用FormData.append添加一个键/值对:<br><code>formData.append('username', 'Chris');</code></p>
<h6>方法二:利用form表单传递给formdata</h6>
<pre><code class="html"><form id="myForm" name="myForm">
<div>
<label for="username">Enter name:</label>
<input type="text" id="username" name="username">
</div>
<div>
<label for="useracc">Enter account number:</label>
<input type="text" id="useracc" name="useracc">
</div>
<div>
<label for="userfile">Upload file:</label>
<input type="file" id="userfile" name="userfile">
</div>
<input type="submit" value="Submit!">
</form></code></pre>
<pre><code class="javascript">var myForm = document.getElementById('myForm');
formData = new FormData(myForm);</code></pre>
<h4>2.formdata上传文件</h4>
<h6>目录结构</h6>
<pre><code>.
├── index.js
├── node_modules
├── package.json
└── public
├── index.html
└── uploads</code></pre>
<h6>客户端代码</h6>
<pre><code class="html"> <!-- index.html -->
<input id="file" type="file">
<button id="btn">点击上传</button>
<img id="img" src="" alt="">
<script>
var btn = document.getElementById("btn"),
file = document.getElementById("file"),
img = document.getElementById("img");
btn.onclick = function () {
// 获取文件
var upload_file = file.files[0],
formdata = new FormData(),
xhr = new XMLHttpRequest();
formdata.append('date',new Date().toLocaleString());
// 将文件添加到formdata对象中,(注:下面的file字段名在node中有用)
formdata.append('file', upload_file);
xhr.open("POST", "/upload", true);
xhr.onreadystatechange = function () {
if (xhr.readyState == 4 && xhr.status == 200) {
img.src = xhr.responseText;
}
}
xhr.send(formdata);
}
</script></code></pre>
<h6>服务端代码</h6>
<pre><code class="javascript">// index.js
var express = require("express");
var app = express();
/*
1. 保存在文件夹中的文件为二进制,所以想在本地点开能预览的,取消下面fs模块引用的注释
2. 并在命令行中输入 npm install fs --save
*/
// var fs = require("fs");
var multer = require("multer");
// 这里dest对应的值是你要将上传的文件存的文件夹
var upload = multer({dest:'./public/uploads'});
app.use(express.static('./public'));
app.post("/upload", upload.single('file'),(req, res) => {
// req.file 是 'file' 文件的信息 (前端传递的文件类型在req.file中获取)
// req.body 将具有文本域数据,如果存在的话 。(上面前端代码中传递的date字段在req.body中获取)
console.log(req.body) //{ date: '2018/1/20 下午5:25:56' }
// ---------- 因为保存的文件为二进制,取消下面代码块注释可让保存的图片文件在本地文件夹中预览 ------
/*
var file_type;
if (req.file.mimetype.split('/')[0] == 'image') file_type = '.' + req.file.mimetype.split('/')[1];
if (file_type) {
fs.rename(req.file.path, req.file.path + file_type, function (err, doc) {
if (err) {
console.error(err);
return;
}
console.log('55');
res.send('./uploads/' + req.file.filename + file_type)
})
return;
}
*/
// ---------------------
res.send('./uploads/' + req.file.filename)
})
app.listen(9999);</code></pre>
<h6>接下来解释下上述代码</h6>
<blockquote>上面的例子是上传图片,服务端传回图片路径进行展示。上传其他文件同理!</blockquote>
<p>index.js中依赖express、multer 可以通过<code>npm install express multer --save</code>进行安装,当然你也可以不使用express。<br>接下去重点讲述下multer:<br>1.安装:<br><code>npm install --save multer</code><br>2.使用:<br><strong>multer(opts)</strong></p>
<blockquote>Multer 接受一个 options 对象,其中最基本的是 dest 属性,这将告诉 Multer 将上传文件保存在哪。如果你省略 options 对象,这些文件将保存在内存中,永远不会写入磁盘。通常,只需要设置 dest 属性 像这样:<br><code>var upload = multer({ dest: 'uploads/' }) // dest对应的值就是你想文件存储的文件夹</code>
</blockquote>
<p><strong>.single(fieldname) </strong></p>
<blockquote>接受一个以 fieldname 命名的文件。这个文件的信息保存在 req.file。(这里的fieldname指的是formdata.append("file",文件)中的'file'字段。<br>其他方法详见<a href="https://link.segmentfault.com/?enc=7M9DQFs3sN4%2BxOtJPCB%2BgA%3D%3D.uwB2vmmHb6ueHJiy6DYQ0rlsJO80JYKo%2F7zcC9nXH%2FyDzWor3ZpvcqSL9Mt%2B5vcf" rel="nofollow">multer文档</a>。</blockquote>
<p>Multer 会添加一个 <code>body</code> 对象 以及 <code>file</code> 或 <code>files</code> 对象 到 express 的 <code>request</code> 对象中。<code>body</code> 对象包含表单的文本域信息,<code>file</code> 或 <code>files</code> 对象包含对象表单上传的文件信息。</p>
<pre><code class="javascript">app.post("/upload", upload.single('file'),(req, res) => {
// req.file 是 'file' 文件的信息 (前端传递的文件类型在req.file中获取)
// req.body 将具有文本域数据,如果存在的话 。(上面前端代码中传递的date字段在req.body中获取)
console.log(req.body) // { date: '2018/1/20 下午5:25:56' }
res.send('./uploads/'+req.file.filename)
})</code></pre>
<h4>注意事项:</h4>
<p>一、formdata在控制台打印为空<br><img src="/img/bV2mKY?w=280&h=48" alt="clipboard.png" title="clipboard.png"></p>
<pre><code>可通过下面方法获取:</code></pre>
<pre><code class="javascript">var formData = new FormData();
formData.append('username', 'Chris');
formData.append('username', 'Bob');
// get()函数只会返回username附加的第一个值
formData.get('username'); // Returns "Chris"
// getAll()函数将返回username一个数组中的两个值:
formData.getAll('username'); // Returns ["Chris", "Bob"]</code></pre>
<p><a href="https://link.segmentfault.com/?enc=s6g24FAOe9z0xwPtw%2BmNvg%3D%3D.RsXcbiZKRBU%2BAQ1CqO6oNgmP57oqvXoLyTS%2FwtV0J9ie5d6SlhrQZAXNeX0HgWep0QvhO%2F6lcuNHjzIvc16twg%3D%3D" rel="nofollow">更多formdata方法</a></p>
<p>二、如果formdata添加文件成功了的话,还是上传失败,可以看看头部是否为<code>multipart/form-data</code></p>
<blockquote><strong>文章都是学习过程中的总结,如果发现错误,欢迎留言指出</strong></blockquote>