eno

eno 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

eno 赞了文章 · 2016-05-03

深入理解 react-router 路由系统

在 web 应用开发中,路由系统是不可或缺的一部分。在浏览器当前的 URL 发生变化时,路由系统会做出一些响应,用来保证用户界面与 URL 的同步。随着单页应用时代的到来,为之服务的前端路由系统也相继出现了。有一些独立的第三方路由系统,比如 director,代码库也比较轻量。当然,主流的前端框架也都有自己的路由,比如 Backbone、Ember、Angular、React 等等。那 react-router 相对于其他路由系统又针对 React 做了哪些优化呢?它是如何利用了 React 的 UI 状态机特性呢?又是如何将 JSX 这种声明式的特性用在路由中?

一个简单的示例

现在,我们通过一个简易的博客系统示例来解释刚刚遇到的疑问,它包含了查看文章归档、文章详细、登录、退出以及权限校验几个功能,该系统的完整代码托管在 JS Bin(注意,文中示例代码使用了与之对应的 ES6 语法),你可以点击链接查看。此外,该实例全部基于最新的 react-router1.0 进行编写。下面看一下 react-router 的应用实例:

import React from 'react';
import { render, findDOMNode } from 'react-dom';
import { Router, Route, Link, IndexRoute, Redirect } from 'react-router';
import { createHistory, createHashHistory, useBasename } from 'history';

// 此处用于添加根路径
const history = useBasename(createHashHistory)({
  queryKey: '_key',
  basename: '/blog-app',
});

React.render((
  <Router history={history}>
    <Route path="/" component={BlogApp}>
      <IndexRoute component={SignIn}/>
      <Route path="signIn" component={SignIn}/>
      <Route path="signOut" component={SignOut}/>
      <Redirect from="/archives" to="/archives/posts"/>
      <Route onEnter={requireAuth} path="archives" component={Archives}>
        <Route path="posts" components={{
          original: Original,
          reproduce: Reproduce,
        }}/>
      </Route>
      <Route path="article/:id" component={Article}/>
      <Route path="about" component={About}/>
    </Route>
  </Router>
), document.getElementById('example'));

如果你以前并没有接触过 react-router,相反只是用过刚才提到的 Backbone 的路由或者是 director,你一定会对这种声明式的写法感到惊讶。不过细想这也是情理之中,毕竟是只服务与 React 类库,引入它的特性也是无可厚非。仔细看一下,你会发现:

  • Router 与 Route 一样都是 react 组件,它的 history 对象是整个路由系统的核心,它暴漏了很多属性和方法在路由系统中使用;

  • Route 的 path 属性表示路由组件所对应的路径,可以是绝对或相对路径,相对路径可继承;

  • Redirect 是一个重定向组件,有 from 和 to 两个属性;

  • Route 的 onEnter 钩子将用于在渲染对象的组件前做拦截操作,比如验证权限;

  • 在 Route 中,可以使用 component 指定单个组件,或者通过 components 指定多个组件集合;

  • param 通过 /:param 的方式传递,这种写法与 express 以及 ruby on rails 保持一致,符合 RestFul 规范;

下面再看一下如果使用 director 来声明这个路由系统会是怎样一番景象呢:

import React from 'react';
import { render } from 'react-dom';
import { Router } from 'director';

const App = React.createClass({
  getInitialState() {
    return {
      app: null
    } 
  },

  componentDidMount() {
    const router = Router({
      '/signIn': {
        on() {
          this.setState({ app: (<BlogApp><SignIn/></BlogApp>) })
        },
      },
      '/signOut': {
        结构与 signIn 类似
      },
      '/archives': {
        '/posts': {
          on() {
            this.setState({ app: (<BlogApp><Archives original={Original} reproduct={Reproduct}/></BlogApp>) })
          },
        },
      },
      '/article': {
        '/:id': {
          on (id) {
            this.setState({ app: (<BlogApp><Article id={id}/></BlogApp>) })
          },
        },
      },
    });
  },

  render() {
    return <div>{React.cloneElement(this.state.app)}</div>;
  },
})
render(<App/>, document.getElementById('example'));

从代码的优雅程度、可读性以及维护性上看绝对 react-router 在这里更胜一筹。分析上面的代码,每个路由的渲染逻辑都相对独立的,这样就需要写很多重复的代码,这里虽然可以借助 React 的 setState 来统一管理路由返回的组件,将 render 方法做一定的封装,但结果却是要多维护一个 state,在 react-router 中这一步根本不需要。此外,这种命令式的写法与 React 代码放在一起也是略显突兀。而 react-router 中的声明式写法在组件继承上确实很清晰易懂,而且更加符合 React 的风格。包括这里的默认路由、重定向等等都使用了这种声明式。相信读到这里你已经放弃了在 React 中使用 react-router 外的路由系统!

接下来,还是回到 react-router 示例中,看一下路由组件内部的代码:

const SignIn = React.createClass({
  handleSubmit(e) {
    e.preventDefault();
    const email = findDOMNode(this.refs.name).value;
    const pass = findDOMNode(this.refs.pass).value;
    // 此处通过修改 localStorage 模拟了登录效果
    if (pass !== 'password') {
      return;
    }
    localStorage.setItem('login', 'true');
    const location = this.props.location;
    if (location.state && location.state.nextPathname) {
      this.props.history.replaceState(null, location.state.nextPathname);
    } else {
      // 这里使用 replaceState 方法做了跳转,但在浏览器历史中不会多一条记录,因为是替换了当前的记录
      this.props.history.replaceState(null, '/about');
    }
  },

  render() {
    if (hasLogin()) {
      return <p>你已经登录系统!<Link to="/signOut">点此退出</Link></p>;
    }
    return (
      <form onSubmit={this.handleSubmit}>
        <label><input ref="name"/></label><br/>
        <label><input ref="pass"/></label> (password)<br/>
        <button type="submit">登录</button>
      </form>
    );
  }
});

const SignOut = React.createClass({
  componentDidMount() {
    localStorage.setItem('login', 'false');
  },

  render() {
    return <p>已经退出!</p>;
  }
})

上面的代码表示了博客系统的登录以及退出功能。登录成功,默认跳转到 /about 路径下,如果在 state 对象中存储了 nextPathname,则跳转到该路径下。在这里需要指出每一个路由(Route)中声明的组件(比如 SignIn)在渲染之前都会被传入一些 props,具体是在源码中的 RoutingContext.js 中完成,主要包括:

  • history 对象,它提供了很多有用的方法可以在路由系统中使用,比如刚刚用到的 history.replaceState,用于替换当前的 URL,并且会将被替换的 URL 在浏览器历史中删除。函数的第一个参数是 state 对象,第二个是路径;

  • location 对象,它可以简单的认为是 URL 的对象形式表示,这里要提的是 location.state,这里 state 的含义与 HTML5 history.pushState API 中的 state 对象一样。每个 URL 都会对应一个 state 对象,你可以在对象里存储数据,但这个数据却不会出现在 URL 中。实际上,数据被存在了 sessionStorage 中;

事实上,刚才提到的两个对象同时存在于路由组件的 context 中,你还可以通过 React 的 context API 在组件的子级组件中获取到这两个对象。比如在 SignIn 组件的内部又包含了一个 SignInChild 组件,你就可以在组件内部通过 this.context.history 获取到 history 对象,进而调用它的 API 进行跳转等操作。

接下来,我们一起看一下 Archives 组件内部的代码:

const Archives = React.createClass({
  render() {
    return (
      <div>
        原创:<br/> {this.props.original}
        转载:<br/> {this.props.reproduce}
      </div>
    );
  }
});

const Original = React.createClass({
  render() {
    return (
      <div className="archives">
        <ul>
          {blogData.slice(0, 4).map((item, index) => {
            return (
              <li key={index}>
                <Link to={`/article/${index}`} query={{type: 'Original'}} state={{title: item.title}}>
                  {item.title}
                </Link>
              </li>
            )
          })}
        </ul>
      </div>
    );
  }
});

const Reproduce = React.createClass({
  // 与 Original 类似
})

上述代码展示了文章归档以及原创和转载列表。现在回顾一下路由声明部分的代码:

<Redirect from="/archives" to="/archives/posts"/>
<Route onEnter={requireAuth} path="archives" component={Archives}>
  <Route path="posts" components={{
    original: Original,
    reproduce: Reproduce,
  }}/>
</Route>

function requireAuth(nextState, replaceState) {
  if (!hasLogin()) {
    replaceState({ nextPathname: nextState.location.pathname }, '/signIn');
  }
}

上述的代码中有三点值得注意:

  • 用到了一个 Redirect 组件,将 /archives 重定向到 /archives/posts 下;

  • onEnter 钩子中用于判断用户是否登录,如果未登录则使用 replaceState 方法重定向,该方法的作用与 <Redirect/> 组件类似,不会在浏览器中留下重定向前的历史;

  • 如果使用 components 声明路由所对应的多个组件,在组件内部可以通过 this.props.original(本例中)来获取组件;

到这里,我们的博客路由系统基本已经讲完了,希望你能够对 react-router 最基本的 API 及其内部的基本原理有一定的了解。再总结一下 react-router 作为 React 路由系统的特点和优势所在:

  • 结合 JSX 采用声明式的语法,很优雅的实现了路由嵌套以及路由回调组件的声明,包括重定向组件,默认路由等,这归功于其内部的匹配算法,可以通过 URL(准确的说应该是 location 对象) 在组件树中准确匹配出需要渲染的组件。这一点绝对完胜 director 等路由在 React 中的表现;

  • 不需要单独维护 state 表示当前路由,这一点也是使用 director 等路由免不了要做的;

  • 除了路由组件外,还可以通过 history 对象中的 pushStatereplaceState方法进行路由和重定向,比如在 flux 的 store 中想要做一个跳转操作就可以通过该方法完成;

    // 近似于 <Link to={path} state={null}/>
    history.pushState(null, path);
    
    // 近似于 <Redirect from={currentPath} to={nextPath}/>
    history.replaceState(null, nextPath);

当然还有一些其他的特性没有在这里介绍,比如在大型应用中按需载入路由组件、服务端渲染以及整合 redux/relay 框架,这些都是用其他路由系统很难完成的。接下来的部分主要来讲解示例背后的基本原理。

原理分析

在这一部分主要会讲解路由的基本原理,react-router 的状态机特性,在用户点击了 Link 组件后路由系统中到底发生了哪些,前端路由如何处理浏览器的前进和后退功能。

路由的基本原理

无论是传统的后端 MVC 主导的应用,还是在当下最流行的单页面应用中,路由的职责都很重要,但原理并不复杂,即保证视图和 URL 的同步,而视图可以看成是资源的一种表现。当用户在页面中进行操作时,应用会在若干个交互状态中切换,路由则可以记录下某些重要的状态,比如在一个博客系统中用户是否登录、在访问哪一篇文章、位于文章归档列表的第几页。而这些变化同样会被记录在浏览器的历史中,用户可以通过浏览器的前进、后退按钮切换状态,同样可以将 URL 分享给好友。简而言之,用户可以通过手动输入或者与页面进行交互来改变 URL,然后通过同步或者异步的方式向服务端发送请求获取资源(当然,资源也可能存在于本地),成功后重新绘制 UI,原理如下图所示:

react-router 的状态机特性

我们看到 react-router 中的很多特性都与 React 保持了一致,比如它的声明式组件、组件嵌套,当然也包括 React 的状态机特性,因为毕竟它就是基于 React 构建并且为之所用的。回想一下在 React 中,我们把组件比作是一个函数,state/props 作为函数的参数,当它们发生变化时会触发函数执行,进而帮助我们重新绘制 UI。那么在 react-router 中将会是什么样子呢?在 react-router 中,我们可以把 Router 组件看成是一个函数,Location 作为参数,返回的结果同样是 UI,二者的对比如下图所示:

上图说明了只要 URL 一致,那么返回的 UI 界面总是相同的。或许你还很好奇在这个简单的状态机后面究竟是什么样子呢?在点击 Link 后路由系统发生了什么?在点击浏览器的前进和后退按钮后路由系统又做了哪些?那么请看下图:

接下来的两部分会对上图做详细的讲解。

点击 Link 后路由系统发生了什么?

Link 组件最终会渲染为 HTML 标签 <a>,它的 to、query、hash 属性会被组合在一起并渲染为 href 属性。虽然 Link 被渲染为超链接,但在内部实现上使用脚本拦截了浏览器的默认行为,然后调用了 history.pushState 方法(注意,文中出现的 history 指的是通过 history 包里面的 create*History 方法创建的对象,window.history 则指定浏览器原生的 history 对象,由于有些 API 相同,不要弄混)。history 包中底层的 pushState 方法支持传入两个参数 state 和 path,在函数体内有将这两个参数传输到 createLocation 方法中,返回 location 的结构如下:

location = {
  pathname, // 当前路径,即 Link 中的 to 属性
  search, // search
  hash, // hash
  state, // state 对象
  action, // location 类型,在点击 Link 时为 PUSH,浏览器前进后退时为 POP,调用 replaceState 方法时为 REPLACE
  key, // 用于操作 sessionStorage 存取 state 对象
};

系统会将上述 location 对象作为参数传入到 TransitionTo 方法中,然后调用 window.location.hash 或者 window.history.pushState() 修改了应用的 URL,这取决于你创建 history 对象的方式。同时会触发 history.listen 中注册的事件监听器。

接下来请看路由系统内部是如何修改 UI 的。在得到了新的 location 对象后,系统内部的 matchRoutes 方法会匹配出 Route 组件树中与当前 location 对象匹配的一个子集,并且得到了 nextState,具体的匹配算法不在这里讲解,感兴趣的同学可以点击查看,state 的结构如下:

nextState = {
  location, // 当前的 location 对象
  routes, // 与 location 对象匹配的 Route 树的子集,是一个数组
  params, // 传入的 param,即 URL 中的参数
  components, // routes 中每个元素对应的组件,同样是数组
};

在 Router 组件的 componentWillMount 生命周期方法中调用了 history.listen(listener) 方法。listener 会在上述 matchRoutes 方法执行成功后执行 listener(nextState),nextState 对象每个属性的具体含义已经在上述代码中注释,接下来执行 this.setState(nextState) 就可以实现重新渲染 Router 组件。举个简单的例子,当 URL(准确的说应该是 location.pathname) 为 /archives/posts 时,应用的匹配结果如下图所示:

对应的渲染结果如下:

<BlogApp>
  <Archives original={Original} reproduce={Reproduce}/>
</BlogApp>

到这里,系统已经完成了当用户点击一个由 Link 组件渲染出的超链接到页面刷新的全过程。

点击浏览器的前进和后退按钮发生了什么?

可以简单地把 web 浏览器的历史记录比做成一个仅有入栈操作的栈,当用户浏览器到某一个页面时将该文档存入到栈中,点击「后退」「前进」按钮时移动指针到 history 栈中对应的某一个文档。在传统的浏览器中,文档都是从服务端请求过来的。不过现代的浏览器一般都会支持两种方式用于动态的生成并载入页面。

location.hash 与 hashchange 事件

这也是比较简单并且兼容性也比较好的一种方式,详细请看下面几点:

  • 使用 hashchange 事件来监听 window.location.hash 的变化

  • hash 发生变化浏览器会更新 URL,并且在 history 栈中产生一条记录

  • 路由系统会将所有的路由信息都保存到 location.hash

  • 在 react-router 内部注册了 window.addEventListener('hashchange', listener, false) 事件监听器

  • listener 内部可以通过 hash fragment 获取到当前 URL 对应的 location 对象

  • 接下来的过程与点击 <Link/> 时保持一致

当然,你会想到不仅仅在前进和后退会触发 hashchange 事件,应该说每次路由操作都会有 hash 的变化。确实如此,为了解决这个问题,路由系统内部通过判断 currentLocation 与 nextLocation 是否相等来处理该问题。不过,从它的实现原理上来看,由于路由操作 hash 发生变化而重复调用 transitonTo(location) 这一步确实无可避免,这也是我在上图中所画的虚线的含义。

这种方法会在浏览器的 URL 中添加一个 # 号,不过出于兼容性的考虑(ie8+),路由系统内部将这种方式(对应 history 包中的 createHashHistory 方法)作为创建 history 对象的默认方法。

history.pushState 与 popstate 事件

新的 HTML5 规范中还提出了一个相对复杂但更加健壮的方式来解决该问题,请看下面几点:

  • 上文中提到了可以通过 window.history.pushState(state, title, path) 方法(更多关于 history 对象的详细 API 可以查看这里)来改变浏览器的 URL,实际上该方法同时在 history 栈中存入了 state 对象。

  • 在浏览器前进和后退时触发 popstate 事件,然后注册 window.addEventListener('popstate', listener, false) ,并且可以在事件对象中取出对应的 state 对象

  • state 对象可以存储一些恢复该页面所需要的简单信息,上文中已经提到 state 会作为属性存储在 location 对象中,这样你就可以在组件中通过 location.state 来获取到

  • 在 react-router 内部将该对象存储到了 sessionStorage 中,也就是上图中的 saveState 操作

  • 接下来的操作与第一种方式一致

使用这种方式(对应 history 包中的 createHistory 方法)进行路由需要服务端要做一个路由的配置将所有请求重定向到入口文件位置,你可以参考这个示例,否则在用户刷新页面时会报 404 错误。

实际上,上面提到的 state 对象不仅仅在第二种路由方式中可以使用。react-router 内部做了 polyfill,统一了 API。在使用第一种方式创建路由时你会发现 URL 中多了一个类似 _key=s1gvrm 的 query,这个 _key 就是为 react-router 内部在 sessionStorage 中读取 state 对象所提供的。

资源汇总

关于 react-router 的参考资源确实不多。特别是 1.0 版本发布后很多文档都已经过时了,所以大家在查阅的时候一定要小心。此外,为了方便读者更好的理解 react-router 的底层原理,也找了一个相关的资源供大家参考。

前导知识

这里汇集了一些关于 url fragment 以及 html5 history API 相关的部分资源:

react-router 相关资源

这里主要是 react-router 的资源。由于 react-router 1.0 相对于之前版本的 API 差异较大,目前网络上的资源也主要是官方文档,不过中文版已经翻译好,读者可以按照喜好选择:

视频资源(需翻墙)

查看原文

赞 31 收藏 157 评论 2

eno 赞了文章 · 2015-12-20

总结个人2015提高前端效率的方法和工具

更多前端内容http://www.codefrom.com/p/JavaScript

阅读之前


非Mac用户请自动忽略,此文针对Mac用户,我个人使用频率非常高,PC端开发者可以选择性的尝试部分工具。

终端工具


安装homebrew,不装真对不起自己

ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

homebrew

换掉/bin/bash请使用/bin/zsh,安装oh-my-zsh。

sh -c "$(curl -fsSL https://raw.github.com/robbyrussell/oh-my-zsh/master/tools/install.sh)"

oh-my-zsh

使用homebrew安装tmux

brew install tmux

tmux

安装vim bundle

git clone https://github.com/gmarik/Vundle.vim.git ~/.vim/bundle/vundle

Vundle.vim
安装nvm来管理node环境

brew install nvm

nvm
安装Xcode command line tools

xcode-select --install

运行命令后,按照指引,你将完成 Xcode command line tools 安装
目前个人使用Atom编辑器和vim结合开发”前端”,如果终端版vim使用不习惯,可以用Macvim(GUI)
Atom

macvim

安装iTerm 2,你懂的

iTerm 2

我使用Dash来查询API文档

Dash

上述终端工具的配置可使用我的配置文件 | 使用指南

git clone https://github.com/lcepy/icepy.vim.git ~/icepy.vim

cp -r .vimrc ~/

cp -r .vimrc.bundles ~/

cp -r .tmux ~/

cp -r .tmux.conf ~/

cp -r .zshrc ~/

或者

cp -r ~/icepy.vim/* ~/

icepy vim shell conf

使用提示

如果不想使用YouCompleteMe可以注释掉,因为此需要Apple LLVM clang编译器
vim目录,可以按F5
在终端输入tmux,control+b (按一下)然后再按D(注意是大写),如果想关闭control+b 再按shift+7
使用nvm install v0.12.6 安装node环境
sudo npm install -g npm (记得更新一下npm)
我使用git来管理我的源代码
:BundleInstall 或者 vim+PluginInstall+qall 来安装vim插件

我常用的“前端”环境(包括node+mongodb+Hybrid App)


我使用TypeScript来编写JavaScript代码,它的类型系统,面向对象的方式比较符合我的预期,而且我能在编译环节就可以排除错误,基本上团队写的代码都一样,维护起来不会出现JavaScript有一千个哈姆雷特的情况,况且TypeScript也是下一代Angularjs所采用的。

sudo npm install -g typescript

tsc helloworld.ts

sudo npm install -g tsd

TypeScript

d.ts

我用tsd来处理第三方模块,这样才能让编译器通过编译。

node开发环境

forever可以在后台启用node服务,好用。
forever
开发阶段使用它无需自己在终端不停的重启node服务
node-supervisor
调试debug工具
node-inspector
mongodb开发环境

brew install mongodb

配置文件在/usr/local/etc/mongod.conf

systemLog:
      destination: file
      path: /usr/local/var/log/mongodb/mongo.log
      logAppend: true
storage:
      dbPath: /Users/xiangwenwen/mongodb/data
net:
      bindIp: 127.0.0.1

在终端运行mongod —config /usr/local/etc/mongod.conf
tmux session右窗口处运行mongo
浏览器端开发环境
主要使用bower来管理我的前端文件,虽然我不写CSS,但是我依然选择sassc来编译,这是C语言写的编译器可以翻译成CSS。

npm install -g bower

brew install sassc

模块化和构建工具
关于模块化和构建工具,三年前使用着grunt和RequireJS,AMD CMD真没啥好争论的,一年前一直用着glup,而今年我准备将使用ES6 modules来做模块化,自动构建工具grunt+glup结合的方式。

npm install -g grunt-cli

npm install -g glup

JavaScript
目前基本上已经全线使用ES5了,所以还要针对IE系列开发的同学可以忽略,HTML5特性也大量的在使用中,ES2015会是今年重点学习的方向。
promises管理异步JavaScript是我处理回调的首选选择。
代码质量,这个目前基本不做了,因为TypeScript
ESLint
JSCS
客户端模版
目前我在使用jade,今年开始使用ES2015内置的模版。
调试工具
一个就够了,chrome tools。
或者option + command + i Atmo调起chrome的调试工具,跟编辑器亲密合体。
Hybrid App
目前在使用ionic,尝试React Native。

npm install -g cordova ionic

ionic
React Native
Hybrid App 调试工具,主要分为两个部分,一个是safari,另外一个是自己编写的iOS版调试日志工具MFLog,可以嵌入到Hybrid App中,分为Objective-C和Swift版。
我常用的”前端”库或插件


移动版的jQuery
zepto
图表绘制库
echarts
工具类函数
lodash
模块化
webpack
requirejs
大框架
Angular
Angular UI
Angular UI
Cordova Plugins
cordova plugins
动画库
famous
构建API
restify
dozerjs
CMS
keystonejs
Express 构建网站
Express
mongoose
mongoose
CSS UI
Bootstrap
Foundation
netease
async
async
hexo编写博客
hexo
我常用的其他工具
用来编写Api 契约文档
apiary
模拟数据
mockjs
查看CSS HTML5的支持情况
caniuse
研究浏览器引擎内核的文献
浏览器的工作原理:新式网络浏览器幕后揭秘
写在最后,我使用频率最高的工具是Github^_^。

查看原文

赞 7 收藏 56 评论 0

eno 回答了问题 · 2015-12-19

解决Z Shell启动速度慢是什么原因?

已找到原因,因为NVM造成的。

export NVM_DIR="/Users/xxx/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"  # This loads nvm

关注 5 回答 5

eno 提出了问题 · 2015-12-18

解决Z Shell启动速度慢是什么原因?

在Mac上按oh-my-zsh官方文档装的,但每次shell打开都要等3秒左右,是什么原因?

默认配置文件.zshrc只开启了:

export ZSH=/Users/xxx/.oh-my-zsh
ZSH_THEME="robbyrussell"
plugins=(git)
source $ZSH/oh-my-zsh.sh
source ~/.bash_profile
export NVM_DIR="/Users/xxx/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"  # This loads nvm

关注 5 回答 5

eno 回答了问题 · 2015-12-02

关于 shell 脚本后台执行的问题

当我们注销或者屏保后,使用&的脚本就会自动停止。
可以试下 nohup 命令

关注 4 回答 2

eno 提出了问题 · 2015-11-04

解决编译同样的scss,为什么gulp的速度几乎是grunt的两倍?

编译同样的scss,为什么gulp的速度几乎是grunt的两倍?
请说明原理,如果有代码解析就更好了。

关注 3 回答 1

认证与成就

  • 获得 6 次点赞
  • 获得 7 枚徽章 获得 0 枚金徽章, 获得 2 枚银徽章, 获得 5 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2015-04-21
个人主页被 162 人浏览