Giter Club home page Giter Club logo

geniusfunny.github.io's People

Contributors

geniusfunny avatar

Stargazers

 avatar

Watchers

 avatar

geniusfunny.github.io's Issues

前端路由的设计**与实现 && React-Router v4🤦‍♂️

前端路由的设计**与实现 && React-Router v4

前端路由的设计

前端路由分为两种实现方式:

  1. HashHistory,通过修改location.hash,然后监听hashchange事件来进行对应操作。
  2. BroswerHistory,通过HTML5提供的History对象的pushStatereplaceStateAPI,然后监听popstate事件来进行对应操作。

二者的优劣:

第一种方法兼容性更好,但是实现比较复杂,并且url比较丑陋(例如:http://www.test.com/#/xxx)。

第二种方法是浏览器提供的能力,所以实现比较简单,url与正常相同(例如:http://www.test.com/xxx),但是可能存在兼容性问题

其实还有第三种,MemoryHistory,用于non-DOM环境下,例如React Native。

基于hash的前端路由实现

_routes用来存储路由对应的回调函数,当hash变化时调用refresh函数(从_routes总找到对应的回调函数然后执行);通过back函数进行回退,所以我们需要引入布尔值判定当前操作是否为回退。

class HashRouter {
    constructor() {
        this._routes = new Map()
        this._history = []
        this._currentUrl = ''
        this._backIndex = this._routes.length - 1
        this._currentIndex = this._routes.length - 1
        this._isBack = false
        this._bindEvent()
    }
    _bindEvent = () => {
        window.addEventListener('laod', this.refresh, false)	// 加载后就触发更新
        window.addEventListener('hashchange', this.refresh, false)	//监听hash的变化,然后调用更新函数
    }
    route(path, cb = function() {}) {
        this._routes.set(path, cb)
    }
    refresh = () => {
        this._currentUrl = location.hash.slice(1) || '/' //读取url中'#'后面的内容
        this._history.push(this._currentUrl)
        this._currentIndex++
        if (!this._isBack) {
            this._backIndex = this._currentIndex
        }
        let cb = this._routes.get(this._currentUrl)
        if (typeof cb === 'function') cb()	//	执行路由对应的回调函数
        this._isBack = false
    }
    back = () => {
        this._isBack = true
        if (this._backIndex <= 0) {
            this._backIndex = 0
        } else {
            this._backIndex -= 1
        }
        location.hash = `#{this._history[this._backIndex]}`
    }
}

基于History的前端路由实现

依赖于HTML5提供的History对象,Router的实现简便很多;使用go来进行前往指定路由,用back回退。

class BroswerRouter {
    constructor() {
        this._routes = new Map()
        this._bindEvent()
    }
    _bindEvent = () => {
        window.addEventListener('popstate', e => {
            let path = e.state && e.state.path
            let cb = this._routes.get(path)
            if (typeof cb = 'function') cb()
        })
    }
    init(path) {
        history.replaceState({path: path}, '', path)
        let cb = this._routes.get(path)
        if (typeof cb = 'function') cb()
    }
    route(path, cb = function() {}) {
        this._routes.set(path, cb)
    }
    go = (path) => {
        history.pushState({path: path}, '', path)
        let cb = this._routes.get(path)
        if (typeof cb === 'function') cb()
    }
    back = () => {
        history.back()
    }
}

React-Router v4

👆👆👆只是简单的前度路由的模拟,但是涉及到React或者Vue的时候,我们需要另外处理渲染逻辑,所以会牵扯到生命周期和组件通信。

下面简单分析概述一下React-Router v4

模块结构

React-Router分为4个package,分别为:react-router、react-router-dom、react-router-native、react-router-config。

react-router:负责通用的路由逻辑,被其他package所依赖,无需特别引入。

react-router-dom: 负责浏览器的路由管理。

react-router-native:负责react-native的路由管理。

react-router-config: 用于配置特别的react-router,比如SSR环境下。

下面介绍一下react-router中只要的模块👇👇👇

Router(react-router/Router、react-router/RouterContext、react-router-dom/BrowserRouter)

BroswerRouter在内部创建一个全局对象history,然后通过props传递给Router组件,Router组件再将这个history属性作为context传递给子组件。

//	创建context,用于全局传递信息
const createNamedContext = name => {
  const context = React.createContext();
  context.Provider.displayName = `${name}.Provider`;
  context.Consumer.displayName = `${name}.Consumer`;
  return context;
}
const RouterContext = createNamedContext('Router')

class Router extends Component {
  static computeRootMatch(pathname) {
    return { path: "/", url: "/", params: {}, isExact: pathname === "/" };
  }
  constructor(props) {
    super(props);
    this.state = {
      location: props.history.location
    };

    // This is a bit of a hack. We have to start listening for location
    // changes here in the constructor in case there are any <Redirect>s
    // on the initial render. If there are, they will replace/push when
    // they mount and since cDM fires in children before parents, we may
    // get a new location before the <Router> is mounted.
    this._isMounted = false;
    this._pendingLocation = null;
    if (!props.staticContext) {
        //	!!!在这里进行监听,每次路由变化都会触发顶层Router的回调事件,然后Router进行setState,再向下传递context,最下面的Route根据context内容判断是否进行渲染!!!
      this.unlisten = props.history.listen(location => {
        if (this._isMounted) {
          this.setState({ location });
        } else {
          this._pendingLocation = location;
        }
      });
    }
  }

  componentDidMount() {
    this._isMounted = true;

    if (this._pendingLocation) {
      this.setState({ location: this._pendingLocation });
    }
  }
  componentWillUnmount() {
    if (this.unlisten) this.unlisten();
  }
  render() {
    return (
      <RouterContext.Provider
        children={this.props.children || null}
        value={{
          history: this.props.history,
          location: this.state.location,
          match: Router.computeRootMatch(this.state.location.pathname),
          staticContext: this.props.staticContext
        }}
      />
    );
  }
}
class BrowserRouter extends React.Component {
  history = createHistory(this.props);
  render() {
    return <Router history={this.history} children={this.props.children} />;
  }
}
Route(react-router/Route)

Router是借助context向Route传递内容的,Router作为Provider、Route作为Comsumer,所以Route必须包含在Router内部。

同时,Route借助context将history、location、match作为三个独立的属性传递给要渲染的组件。

class Route extends Component {
  render() {
    return (
      <RouterContext.Consumer>
        {context => {

          const location = this.props.location || context.location
          const match = this.props.computedMatch
            ? this.props.computedMatch // <Switch> already computed the match for us
            : this.props.path
              ? matchPath(location.pathname, this.props)
              : context.match

          const props = { ...context, location, match }	//	传递给要渲染的组件

          let { children, component, render } = this.props

          // Preact uses an empty array as children by
          // default, so use null if that's the case.
          if (Array.isArray(children) && children.length === 0) {
            children = null;
          }
          if (typeof children === "function") {
            children = children(props);

            if (children === undefined) {
              children = null;
            }
          }

          return (
              //	提供了多种渲染的方式,children(子元素)、component(props)、render(props)
            <RouterContext.Provider value={props}>
              {children && !isEmptyChildren(children)
                ? children
                : props.match
                  ? component
                    ? React.createElement(component, props)	//	这里是通过React.creareElement创建组件的,所以React Diff认为每次都不一样,更新的时候会先卸载之前的组件再重新安装新的组件。
                    : render
                      ? render(props)
                      : null
                  : null}
            </RouterContext.Provider>
          );
        }}
      </RouterContext.Consumer>
    );
  }
}
Switch(react-router/Switch)

Switch的作用是,当Switch中的第一个Route匹配后就不会渲染其他的Route,类似swich这个语法。源码如下:

class Switch extends Component {
  render() {
    return (
      <RouterContext.Consumer>
        {context => {

          const location = this.props.location || context.location;

          let element, match;

          // We use React.Children.forEach instead of React.Children.toArray().find()
          // here because toArray adds keys to all child elements and we do not want
          // to trigger an unmount/remount for two <Route>s that render the same
          // component at different URLs.
          React.Children.forEach(this.props.children, child => {
         //	按children的顺序依次遍历子元素,成功就标记这个子元素和对应的location和comutedMatch,最后调用React.cloneElement渲染,否则返回null
            if (match == null && React.isValidElement(child)) {
              element = child;

              const path = child.props.path || child.props.from;
			//	Switch通过matchPath来判断是否匹配成功
              match = path
                ? matchPath(location.pathname, { ...child.props, path })
                : context.match;
            }
          });

          return match
            ? React.cloneElement(element, { location, computedMatch: match })
            : null;
        }}
      </RouterContext.Consumer>
    );
  }
}
matchPath(react-router/matchPath)

前面多次提到使用matchPath来判断是否匹配成功,那么到底是如何进行匹配的呢?下面是matchPath的源码

//	缓存,不必每次都生成一个正则表达式用于判断
const cache = {};
const cacheLimit = 10000;
let cacheCount = 0;

function compilePath(path, options) {
  const cacheKey = `${options.end}${options.strict}${options.sensitive}`;
  const pathCache = cache[cacheKey] || (cache[cacheKey] = {});

  if (pathCache[path]) return pathCache[path];

  const keys = [];
  const regexp = pathToRegexp(path, keys, options);
  const result = { regexp, keys };

  if (cacheCount < cacheLimit) {
    pathCache[path] = result;
    cacheCount++;
  }

  return result;
}

/**
 * Public API for matching a URL pathname to a path.
 */
function matchPath(pathname, options = {}) {
  if (typeof options === "string") options = { path: options };

  const { path, exact = false, strict = false, sensitive = false } = options;

  const paths = [].concat(path);

  return paths.reduce((matched, path) => {
    if (matched) return matched;
    const { regexp, keys } = compilePath(path, {
      end: exact,
      strict,
      sensitive
    });
    const match = regexp.exec(pathname);

    if (!match) return null;

    const [url, ...values] = match;
    const isExact = pathname === url;

    if (exact && !isExact) return null;

    return {
      path, // the path used to match
      url: path === "/" && url === "" ? "/" : url, // the matched portion of the URL
      isExact, // whether or not we matched exactly
      params: keys.reduce((memo, key, index) => {
        memo[key.name] = values[index];
        return memo;
      }, {})
    };
  }, null);
}
Link(react-router-dom/Link)

Link是react-router-dom中的,而不是通用模块里的元素;Link是一个用a标签包裹、用来实现跳转的元素。

class Link extends React.Component {
  handleClick(event, history) {
    if (this.props.onClick) this.props.onClick(event);

    if (
      !event.defaultPrevented && // onClick prevented default
      event.button === 0 && // ignore everything but left clicks
      (!this.props.target || this.props.target === "_self") && // let browser handle "target=_blank" etc.
      !isModifiedEvent(event) // ignore clicks with modifier keys
    ) {
      event.preventDefault();

      const method = this.props.replace ? history.replace : history.push;

      method(this.props.to);
    }
  }

  render() {
    const { innerRef, replace, to, ...rest } = this.props; // eslint-disable-line no-unused-vars

    return (
      <RouterContext.Consumer>
        {context => {

          const location =
            typeof to === "string"
              ? createLocation(to, null, null, context.location)
              : to;
          const href = location ? context.history.createHref(location) : "";

          return (
            <a
              {...rest}
              onClick={event => this.handleClick(event, context.history)}
              href={href}
              ref={innerRef}
            />
          );
        }}
      </RouterContext.Consumer>
    );
  }
}

H5的history对象中通过pushState、replaceState只会改变路由而不会发生跳转,但是之前提的Router中的listen可以监听到路由的变化然后更新props和context,让下层的Route重新匹配,完成需要渲染部分的更新。如何监听的呢?这部分在history库中的createTransitionManager中(React-Router依赖于history)。

createTransitionManager(history/createTransitionManager)

下面就是源码,观察者模式的一个应用

function createTransitionManager() {
  let prompt = null;

  function setPrompt(nextPrompt) {
    prompt = nextPrompt;
    return () => {
      if (prompt === nextPrompt) prompt = null;
    };
  }

  function confirmTransitionTo(
    location,
    action,
    getUserConfirmation,
    callback
  ) {
    // TODO: If another transition starts while we're still confirming
    // the previous one, we may end up in a weird state. Figure out the
    // best way to handle this.
    if (prompt != null) {
      const result =
        typeof prompt === 'function' ? prompt(location, action) : prompt;

      if (typeof result === 'string') {
        if (typeof getUserConfirmation === 'function') {
          getUserConfirmation(result, callback);
        } else {
         
          callback(true);
        }
      } else {
        // Return false from a transition hook to cancel the transition.
        callback(result !== false);
      }
    } else {
      callback(true);
    }
  }

  let listeners = [];

  function appendListener(fn) {
    let isActive = true;

    function listener(...args) {
      if (isActive) fn(...args);
    }

    listeners.push(listener);

    return () => {
      isActive = false;
      listeners = listeners.filter(item => item !== listener);
    };
  }

  function notifyListeners(...args) {
    listeners.forEach(listener => listener(...args));
  }

  return {
    setPrompt,
    confirmTransitionTo,
    appendListener,
    notifyListeners
  };
}
WithRouter(react-router/WithRouter)

光从名字上看就知道,这是一个HOC,这个高阶组件的作用就是让我们在非Route中的组件也能获取到路由信息。

function withRouter(Component) {
  const C = props => {
    const { wrappedComponentRef, ...remainingProps } = props;

    return (
      <Route
        children={routeComponentProps => (
          <Component
            {...remainingProps}
            {...routeComponentProps}
            ref={wrappedComponentRef}
          />
        )}
      />
    );
  };

  C.displayName = `withRouter(${Component.displayName || Component.name})`;
  C.WrappedComponent = Component;
  return hoistStatics(C, Component);
}

总结

现在我们来大致梳理一下,前端路由的过程。

  1. 当Router组件创建时,我们会在上面注册一个事件用于监听路由的变化,一旦路由变化就会触发setState。
  2. 点击Link其实就是点击a标签进行跳转,但是我们阻止了跳转;实际上我们在这里调用history的pushState方法,将当前path存储,并修改路由(此时并没有改变页面的渲染)。
  3. 每次路由的变化就会触发Router的setState,所以此时会成新context传递给下层。
  4. 下层的Route拿到新的context后根据matchPath判断path是否和location匹配,匹配就渲染,不匹配不渲染,此时页面的渲染改变

所以说整个前端路由的思路(以React-Router为例)就是:

  1. 首先创建一个全局对象,通过context API向下传递histpry对象,从而控制具体的渲染;(渲染变化)
  2. 添加一个Event Emitter,用于响应路由变化/Hash变化,然后进行对应的逻辑处理;(路由变化)
  3. 路由变化/Hash变化导致context变化,context变化又导致渲染改变;(渲染变化)

那么,前端路由跟后端路由有什么区别呢?

后端路由,数据渲染是由服务器解决,例如利用express构建的服务端应用,render('xxx.pug'),服务器渲染好页面后返回给浏览器端,浏览器显示。表现形式就是,跳转页面会白屏,加载~

前端路由,数据渲染是由浏览器端解决,例如上述的例子,但是路由变化和渲染变化是分隔开的,所以我们需要通过hashchange or popstate将路由变化和渲染变化连接起来。表现形式则是,跳转页面不会白屏~

那是不是前端路由就可以完全取代后端路由了呢?不是的,前端路由也存在不足,SEO就是一个big problem。

那是不是前端路由和后端路由就是相互隔绝的呢?不是的,二者之间又衍生出许多新的hack手段,例如预渲染(服务端线构建出一部分静态HTML,用于直出浏览器,然后剩余使用前端渲染来实现)、SSR。

既然谈到了预渲染和SSR;那就先占个坑位,学习一下SSR和预渲染~

参考资源

  1. 前端路由的两种实现原理
  2. 面试官:你了解前端路由吗
  3. React-Router源码
  4. history源码---Manage session history with JavaScript

一个简易版Node进程池

使用进程池提高Node.js并行处理能力

背景

Node是单线程模型,当需要执行多个独立且耗时任务的时候,只能通过child_process来分发任务,提高处理速度;不像Java这种多线程语言,可以通过线程来解决并行问题,Node只能创建进程来进行处理;但是进程相对于线程来说,开销太大。一旦进程数较多时,CPU和内存消耗严重(影响我干其他的事情),所以做了一个简易版的进程池,用来解决并行任务的处理。

思路

主控进程+工作进程群

ProcessPool是我们管理进程的地方,我们通过传递配置参数(任务脚本、脚本需要的参数、最大并行进程数)生成一个ProcessPool实例,然后通过这个实例来管控进程池。

ProcessItem是我们进程池里的进程对象,ProcessItem对象除了process的信息,我们还增加了唯一标识和状态(忙碌、任务失败、任务完成、进程不可用)。

一批任务开始时,我们会一次性fork到最大并行进程数,然后开始监控是否有工作进程完成任务,如果有工作进程完成了任务,那我们就可以复用这个工作进程,让其执行新任务;如果任务执行失败,我们会将任务归还给进程池,等待下一次分发。

由于主控进程即要负责IPC又要不断监听批任务完成的情况,目前我采用的方式是setInterval切割,让IPC和监控能交替进行(ps:应该有更好的方法

实现

ProcessPool.js
const fs = require('fs')
const ProcessItem = require('./ProcessItem')
/**
 * 进程池类
 * @param maxParallelProcess,最大并行工作进程数
 * @param timeToClose,任务最长耗时时间
 * @param taskParams,所有任务脚本需要的参数
 * @param dependency,任务脚本所需依赖
 * @param taskName, 工作脚本名称
 * @param script 脚本内容
 * @param workDir 工作目录
 * Todo: 读写同一文件时出现任务丢失,待修复bug
 */
function ProcessPool({
    maxParallelProcess = 50,
    timeToClose = 60 * 1000,
    taskParams = [],
    dependency = '',
    workDir ='',
    taskName = Date.now(),
    script = '',}) {
  if (typeof script !== 'function' && script.name === 'task') {
    throw new Error('script must be a async function that named task')
  }
  if (typeof maxParallelProcess !== 'number' || maxParallelProcess < 1) {
    throw new Error('maxParallelProcess must be a integer and > 0')
  }
  if (typeof timeToClose !== 'number' || timeToClose < 1) {
    throw new Error('timeToClose must be a integer and > 0')
  }
  if (typeof dependency !== 'string') {
    throw new Error('dependency must be a string')
  }
  if (typeof workDir !== 'string') {
    throw new Error('dependency must be a path')
  }
  this.timeToClose = timeToClose
  this.processList = new Map() // 使用Map存储进程对象
  this.currentProcessNum = 0 // 当前活动进程数
  this.dependency = dependency // 任务脚本依赖
  this.workDir = workDir // 主控函数工作目录
  this.taskName = taskName // 任务脚本名称
  this.task = `${this.workDir}/${this.taskName}.js`// 任务脚本路径
  this.taskParamsTodo = taskParams // 待完成的任务参数数组,包含了n个小任务所需参数,所以是一个二维数组
  this.taskParamsDone = [] // 已完成的任务参数数组
  this.maxParallelProcess = maxParallelProcess // 最大进程并行数
  this.script = script
  try {
    this.writeScript()
  } catch (e) {
    throw new Error('创建工作脚本失败' + e)
  }
}
/**
 * 将内容写进脚本
 */
ProcessPool.prototype.writeScript = function() {
  /**
   * Todo:生产工作进程脚本
   * 1. 在工作目录下新建脚本
   * 2. 注入依赖
   * 3. 引入task模版
   * 4. 倒入任务脚本内容
   */
  if (this.fileIsExist()) {
    fs.writeFileSync(this.task, '')
  }
  try {
    fs.appendFileSync(this.task, `${this.dependency}\n`, (err) => {
      if (err) throw new Error('依赖写入失败')
    })
  } catch (e) {
    throw new Error('依赖写入失败')
  }
  try {
    fs.copyFileSync(`${__dirname}/task.js`, this.task)
  } catch (e) {
    throw new Error('复制task模版失败')
  }
  try {
    fs.appendFileSync(this.task, this.script.toString(), err => {
      if (err) throw new Error('任务脚本写入失败')
    })
  } catch (e) {
    throw new Error('任务脚本写入失败')
  }
}
/**
 * 复用空闲进程
 * @param key,可复用进程的pid
 */
ProcessPool.prototype.reuseProcess = function(key) {
  const workProcess = this.processList.get(key)
  if (this.taskParamsTodo.length) {
    const taskParam = this.taskParamsTodo.shift()
    workProcess.state = 1 // 设置为忙碌
    workProcess.process.send(taskParam)
  }
}
/**
 * 监测当前是否有正在处理任务的进程
 * @returns {number}
 */
ProcessPool.prototype.hasWorkProcessRunning = function() {
  if (!this.processList.size) return 1 // 进程池刚启动,尚无进程
  for (const p of this.processList.values()) {
    if (p.state === 1) return 2 // 有忙碌的进程
  }
  return -1
}
ProcessPool.prototype.listenProcessFinish = function(workProcess, params) {
  workProcess.process.on('message', message => {
    if (message === 'finish') {
      this.taskParamsDone.push(params)
      workProcess.finishTask()
    } else if (message === 'failed') {
      this.taskParamsTodo.unshift(params)
      workProcess.unFinishTask()
    }
  })
}
ProcessPool.prototype.addProcess = function() {
  if (this.currentProcessNum <= this.maxParallelProcess) {
    let workParam = this.taskParamsTodo.shift()
    const newProcess = new ProcessItem({task: this.task, workParam})
    this.processList.set(newProcess.id, newProcess)
    this.currentProcessNum++
    this.listenProcessFinish(newProcess, workParam)
  }
}
/**
 * 从进程池中移除[进程id]
 * @param id
 */
ProcessPool.prototype.removeProcess = function(id) {
  if (this.processList.has(id)) {
    const processToTerminate = this.processList.get(id)
    processToTerminate.terminate()
    this.currentProcessNum--
  }
}
/**
 * 关闭所有进程并清空进程池
 */
ProcessPool.prototype.closeProcessPool = function() {
  const processItems = this.processList.values()
  for (const processItem of processItems) {
    processItem.terminate()
  }
  // 清空进程池
  this.processList = null
  process.kill(process.pid)
}
/**
 * 检查任务脚本是否已经存在
 */
ProcessPool.prototype.fileIsExist = function() {
  fs.access(this.task, fs.constants.F_OK, err => {
    return !err
  })
}
/**
 * 进程池启动,处理任务
 * Todo:一方面要实时监控任务状态,另一方面要处理工作进程传递过来的message,由于单线程模型,二者只有一个能运行,目前采用定时器切换工作上下文,应该有更好的方法。
 *
 */
ProcessPool.prototype.run = function() {
  setInterval(() => {
    let flag = this.hasWorkProcessRunning() // 判断是否有工作进程正在执行或是否是第一次处理任务
    const taskTodoNum = this.taskParamsTodo.length

    if (flag === 1 && taskTodoNum) {
      // 初始阶段,fork min{任务数,最大进程数} 的进程
      while (this.currentProcessNum <= this.maxParallelProcess && this.currentProcessNum <= taskTodoNum) {
        this.addProcess()
      }
    } else if (flag > 0 && !taskTodoNum) {
      // 如果有工作进程正在执行且没有新的任务要执行,那么等待工作进程结束任务
    } else if (flag > 0 && taskTodoNum) {
      // 如果有工作进程正在执行且有新的任务要执行,如果有空闲进程,那么重用空闲进程执行新任务
      const processList = this.processList.values()
      for (const p of processList) {
        if (p.state !== 1 || p.state !== 4) {
          this.reuseProcess(p.id)
        }
      }
    } else if (flag < 0 && taskTodoNum) {
      // 如果没有工作进程正在执行且有新的任务要执行,如果有空闲进程,那么重用空闲进程执行新任务,如果没有则新启动进程进行执行任务
      const processList = this.processList.values()
      for (const p of processList) {
        if (p.state !== 1 || p.state !== 4) {
          this.reuseProcess(p.id)
        }
      }
    } else if (flag < 0 && !taskTodoNum) {
      // 如果没有工作进程正在执行且没有新的任务要执行,关闭进程池,任务完成
      this.closeProcessPool()
    }
  }, 0)
}
module.exports = ProcessPool
ProcessItem.js
const ChildProcess = require('child_process')
/**
 * 工作进程类
 */
function ProcessItem({ task = './task.js', workParam = [] }) {
  /**
   * state 状态码
   * 1: 忙碌
   * 2: 完成任务
   * 3: 未完成任务
   * 4: 不可用
   */
  if (!Array.isArray(workParam)) {
    throw new Error('workParam must be a array')
  }
  if (typeof task !== 'string') {
    throw new Error('workParam must be a string')
  }
  this.process = this.createProcess(task, workParam)
  this.state = 1
  this.id = this.process.pid
}
ProcessItem.prototype.finishTask = function() {
  if (this.state === 1) {
    this.state = 2
  }
}
ProcessItem.prototype.unFinishTask = function() {
  this.state = 3
}
ProcessItem.prototype.terminate = function() {
  try {
    this.process.kill()
    this.state = 4
  } catch (e) {
    throw new Error(`关闭进程${this.id}失败`)
  }
}
ProcessItem.prototype.createProcess = function (task, workParam) {
  let childProcess = ChildProcess.fork(task, workParam)
  if (childProcess) {
    return childProcess
  } else {
    throw new Error('create process failed')
  }
}

module.exports = ProcessItem
Task.js
/**
 * 当进程被子进程创建后,立刻执行工作任务
 */
async function firstTask() {
  const workParam = process.argv.slice(2)
  await task(workParam)
}
/**
 * 完成任务,提示进程池已完成,工作进程空闲
 */
async function finishTask() {
  await process.send('finish')
}
/**
 * 任务失败,提示进程池未完成,归还任务
 */
async function unFinishTask() {
  await process.send('failed')
}
/**
 * 监听进程池后续指派的任务
 */
process.on('message', async workParam => {
  await task(workParam)
  try {
    await finishTask()
  } catch (e) {
    await unFinishTask()
  }
})
/**
 * 进程被创建时立即执行进程池指派的任务
 * @returns {Promise<void>}
 */
async function main() {
  try {
    await firstTask()
    await finishTask()
  } catch (e) {
    await unFinishTask()
  }
}
main()
/**
 * @name 工作进程负责的任务
 * @param workParam // 执行任务所需的参数数组
 * 动态添加任务脚本到此文件尾部
 */

使用方法

安装
npm install node-process-pool
使用
// 进程池使用示例
const ProcessPool = require('../src/ProcessPool')
const taskParams = []
for (let i = 0; i < 100; i++) {
  taskParams[i] = [i]
}
// 创建进程池实例
const processPool = new ProcessPool({
  maxParallelProcess: 50, // 支持最大进程并行数
  timeToClose: 60 * 1000, // 单个任务被执行最大时长
  dependency: `const path = require('path')`, // 任务脚本依赖
  workDir: __dirname, // 当前目录
  taskName: 'test', // 任务脚本名称
  script: async function task(workParam) {
    console.log(workParam)
  }, // 任务脚本内容
  taskParams // 需要执行的任务参数列表,二维数组
})
// 利用进程池进行处理大规模任务
processPool.run()

// 测试任务1:写时间戳到文本中
// 进程池:5000个任务,耗时2.7s,每个任务耗时0.54ms
// 单线程:5000个任务,耗时0.456s,每个任务耗时0.0934ms

// 测试任务2:写时间戳到文本中,但每个任务需要至少耗时20ms(while空循环)
// 进程池:5000个任务,耗时15.089s,每个任务耗时3.02ms
// 单线程:5000个任务,耗时100.260s,每个任务耗时20.052ms

// 显然,当处理独立且耗时任务时,使用进程池更加合适。例如爬取信息,直接会将对方服务器503,2333333~

用JavaScript实现筛选

// source: 需要筛选的原资源
// itemName: 要筛选的属性名
// itemValue: 指定的属性值
const filtrateItem = (source, itemName, itemValue) => {
return source.filter((item) => {
return item[itemName] === itemValue
})
}

// source: 需要筛选的原资源
// 关键字: {name: value}
const filtrate = (source, keyWord) => {
const keyWordName = Object.keys(keyWord)
const keyWordValue = Object.values(keyWord)
keyWordName.map((item, index) => {
source = filtrateItem(source, item, keyWordValue[index])
})
return source
}

浏览器缓存概览

浏览器缓存

当我们谈到浏览器缓存的时候,总是会提到强缓存/协商缓存,但是事实上,浏览器(Chrome)的缓存不止于此。浏览器(以Chrome为例)的缓存分为四种:**Service Worker Cache、Memory Cache、Disk Cache、Push Cache。**而我们所说的强缓存/协商缓存,只是Disk Cache(也叫HTTP Cache)。

接下来,按优先级递减的顺序一一简单介绍这四种类型的Cache。

Service Worker Cache

Service Worker Cache不同于Memory Cache和Disk Cache,它是由我们自己来管控,并且这个缓存是永久性的,必须我们手动删除或者浏览器清空cache storage。

如果 Service Worker 没能命中缓存,一般情况会使用 fetch() 方法继续获取资源。这时候,浏览器就去 memory cache 或者 disk cache 进行下一次找缓存的工作了。

Memory Cache

Memory Cache,存储在内存中。几乎所有的资源都会自动放在内存中,但是由于内存大小有限制,所以只能存放一定数量的数据,并且浏览器一旦关闭Tab,存储在内存中的数据就会丢失。

preloader:边解析js/css,边获取js/css

preload:显式指定的预加载资源,也会被放入 memory cache 中。

cache:no store,只有这样设置才会让资源不被短期缓存,其余的方式都会存在短期缓存效果(毕竟资源还在内存里嘛)

Disk Cache

也就是HTTP Cache,这个可以简单介绍。

强缓存:

  1. Cache-Control:max-age=xxx;
  2. Expire:绝对时间

协商缓存:

  1. Last-Modified & If-Modified-Since
  2. Etag & If-None-Match
  3. Etag 优先于 Last-Modified

Push Cache

HTTP/2 中的 Server Push 技术使得服务端收到页面请求后,能够将页面所需资源(CSS、JS)通过 PUSH_PROMISE 帧推送给浏览器,从而减少延迟;如果要推送的资源浏览器本地已经有了缓存,会导致流量的浪费。

HTTP/2的服务端推送,推送的资源直接进入客户端缓存

最佳实践

  1. 不变的内容:max-age=超大的数字
  2. 变化的内容:Cache-Control:no-cache(no-cache不是不缓存,只是用的时候会向服务端验证有效性)

参考资料

  1. Caching best practices & max-age gotchas
  2. A Tale of Four Caches

Node.js系列专题之Express分析

Node.js系列专题之Express源码解读

介绍

Node.js的发展历史

2009年,Node.js诞生

2010年,Express和Socket.io诞生

2011年,Node.js正式商用(LinkedIn、Uber)、Npm诞生

2012年,Node.js趋于成熟,商用框架Hapi诞生

2013年,Node.js在Web框架领域百花齐放,Koa诞生;蚂蚁金服内部推出Chair框架(Egg.js前身)

2016年,Egg.js开源

定义

官方对Express的描述:Fast, unopinionated, minimalist web framework for Node.js

我们可以从中看出,Express主要有2个特点:

  • 快速极简:通过扩展 Node 的功能来提高开发效率
  • 高度包容:框架不会对开发者过多的限制,可以自由发挥想象的空间

Demo

const express = require('express')
const app = express() // 实际执行 createcreateApplication
const port = 3000

app.use(function(req, res, next) {
  // ...
  next()
})
app.use('/test', function(req, res, next) {
  // ...
  next()
})


app.get('/', (req, res) => {
  res.send('Hello World!')
})

app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`)
})

Demo非常简单,主要步骤如下:

1.引入express模块

2.调用express(),创建一个应用实例

3.通过app.use绑定两个中间件

4.通过app.get为根目录注册一个Get路由

5.通过app.listen将服务运行在3000端口,并监听请求

源码机构

Express源码合计约3500行。

- lib/
    - middleware/
        - init.js
        - query.js
    - router/
        - index.js
        - layer.js
        - route.js
    - application.js
    - express.js
    - request.js
    - response.js
    - utils.js
    - view.js
- index.js

  • middleware目录下包含两个默认的中间件
  • router目录下主要是路由功能的源码【核心】
  • request/response主要是简化和增强了http模块原始的req/res对象
  • application.js跟应用实例相关,掌握整个程序的流程控制【核心】
  • express.js里面暴露了createApplication这个工厂函数,用于创建应用实例
  • utils/view则分别是express框架所用到的工具函数和模板引擎代码
  • index.js仅仅是将express.js的导出再此导出

核心实现

下面主要从程序运行角度,将express的源码分为两个阶段:初始化流程请求处理

初始化流程

创建应用实例
function createApplication() {
  var app = function(req, res, next) {
    app.handle(req, res, next);
  };

  mixin(app, EventEmitter.prototype, false); // 赋予eventEmitter能力
  mixin(app, proto, false); // 挂载基本方法,例如get/post/listen/init/use/handle等

  // expose the prototype that will get set on requests
  app.request = Object.create(req, {
    app: { configurable: true, enumerable: true, writable: true, value: app }
  })

  // expose the prototype that will get set on responses
  app.response = Object.create(res, {
    app: { configurable: true, enumerable: true, writable: true, value: app }
  })
	
  app.init(); // 初始化
  return app;
}

Demo中调用的express()即执行的createApplication函数;

  1. 在createApplication函数执行过程中,app这个函数被挂载了许多方法和属性。

  2. 然后调用app.init()方法进行初始化。

  3. 最后返回app函数。(为什么返回的是函数而不是Object?)

挂载中间件(一)
app.use = function (fn) {
  // 通过offset来处理函数重载,忽略细节
  var fns = flatten(slice.call(arguments, offset));
  this.lazyrouter() // 初始化router,讲路由的时候再说
  fns.forEach(fn => {
    // 实际上app.use调用的router的中间件绑定
    this.router.use(path, function mounted_app(req, res, next) {
    	fn.handle(req, res, function(err) {
      setPrototypeOf(req, orig.request)
      setPrototypeOf(res, orig.response)
      next(err);
    })
  })
  fn.emit('mount', this);
  })
}

绑定中间件:

  1. 通过对第一个实参的类型进行判断进而达到函数重载的效果
    • 若为string类型,则第一个参数为 path(默认’/’);offset设置为1
    • 若为function类型,则参数都为中间件;offset设置为0
  2. 遍历中间件数组,依次调用router.use方法进行绑定(app.use实际执行router.use)
创建Router
// 单例模式
app.lazyrouter = function lazyrouter() {
  if (!this._router) {
    // 初始化一个router实例,全局唯一
    this._router = new Router({
      caseSensitive: this.enabled('case sensitive routing'),
      strict: this.enabled('strict routing')
    });
		// router挂载基本中间件
    this._router.use(query(this.get('query parser fn')));
    this._router.use(middleware.init(this));
  }
};

通过单例模式初始化了一个Router实例,然后挂载基本中间件。

Router对象内部结构:

function Router(options) {
  var opts = options || {};

  function router(req, res, next) {
    router.handle(req, res, next);
  }

  // mixin Router class functions
  setPrototypeOf(router, proto)

  router.params = {};
  router._params = [];
  router.caseSensitive = opts.caseSensitive;
  router.mergeParams = opts.mergeParams;
  router.strict = opts.strict;
  router.stack = [];

  return router;
};

Router构造函数:

  1. 与createApplication类似,返回的也是函数而非object

  2. 内部维护了stack数组

挂载中间件(二)

app.use内部实际执行的router.use

// router.use(fn1, fn2); // 所有路径和方法(除options)都会执行
// router.use('/test', fn3); // 针对/test才会执行

// 伪代码
router.use = function(callbacks) {
  // 
  for(let i = 0; i < callbacks.length; i++) {
    const layer = new Layer(path, {
      // 配置
    }, callbacks[i]);
    layer.route = undefined;
    this.stack.push(layer); // 将所有中间件维护在一起
  }
}

1.与app.use类似,通过对参数的判断进行函数重载

2.对每个中间件包装成Layer对象,Layer对象的route属性设置为undefined;

3.将layer对象维护在router的stack数组中

这里的layer对象内部机构如下:

function layer(path, options, fn) {
  // 省略部分代码
   this.handle = fn;
   this.path = undefined;
}

Layer构造函数: 接受3个参数,第一个是路径,默认为undefined;最后一个是处理函数

注册路由
  1. methods为HTTP方法数组,源码灵活的为app绑定注册路由的方法
  2. 注册路由实际还是调用的router内部的方法即router.route
  3. 执行route[method]方法,为route绑定处理函数
app[method] = function(path){
  if (method === 'get' && arguments.length === 1) {
    // app.get(setting)
      return this.set(path);
   }
    this.lazyrouter();
    var route = this._router.route(path); // 初始化route
    route[method].apply(route, slice.call(arguments, 1)); // 绑定处理函数
    return this;
}

初始化route:

1.创建指定path的route对象

2.创建指定path的layer对象,并且该layer绑定的函数为route.dispatch

3.并将layer推入router的stack数组

app.use绑定中间件时创建layer时,将layer.route设置为undefined,注册路由时却将layer.route设置为route。

由此可知,Router的layer有2种类型,一种非路由中间件(未指定route),一种则为路由中间件(指定route)。

router.route = function route(path) {
  let route = new Route(path);
  let layer = new Layer(path, {
    // 配置
  }, route.dispatch.bind(route)) // layer上绑定route的dispatch方法【后续会说作用】
  layer.route = route; // layer只与此route相关
  this.stack.push(layer); // 将一个route下的所有路由函数放一起
  return route;
}

Route对象内部结构:

  • 必须指定path
  • 拥有stack数组
  • 还有一个methods对象
function Route(path) {
  // 忽略部分代码
  this.path = path;
  this.stack = []; // route也有stack,用来存储处理函数,即对应 http路由的 处理函数
  // route handlers for various http methods
  this.methods = {};
}
注册路由处理函数

Route为指定方法绑定处理函数:

  1. 同时利用了methods数组,遍历绑定
  2. 用layer包装处理函数,使用stack统一管理
methods.forEach(function(method){
  Route.prototype[method] = function(){
    var handles = flatten(slice.call(arguments));

    for (var i = 0; i < handles.length; i++) {
      var handle = handles[i];

      if (typeof handle !== 'function') {
        var type = toString.call(handle);
        var msg = 'Route.' + method + '() requires a callback function but got a ' + type
        throw new Error(msg);
      }

      var layer = Layer('/', {}, handle);
      layer.method = method;

      this.methods[method] = true;
      this.stack.push(layer);
    }

    return this;
  };
});
总结

经过初始化阶段后,各个模块关系图如下:

初始化模块关系图.png

  • Router下面有多个Layer(可以理解为中间件),中间件分2大类:
    • 通过app.use或者router.use绑定的非路由中间件;中间件的功能由开发者自定义;
    • (以get为例)通过app. get或者router.get绑定的路由中间件;中间件是由框架指定Route.dispatch方法;
  • Route下面也有多个Layer(也可以理解为中间件),即注册路由时的回调函数;例如:app.get(‘/test’, fn); fn就是回调函数

请求处理

端口监听

Express将创建server和监听端口合并成一个app.listen函数

app.listen = function listen() {
  var server = http.createServer(this);
  return server.listen.apply(server, arguments);
};

并且,创建server时 将app函数本身作为requestListener,那么每次端口监听到请求时就会执行app函数;

http.createServer

这样就进入express的第二个阶段,请求处理阶段

app.handle

请求处理

app.handle内部实际调用的router.handle

app.handle = function handle(req, res, callback) {
  var router = this._router;

  // final handler
  var done = callback || finalhandler(req, res, {
    env: this.get('env'),
    onerror: logerror.bind(this)
  });

  // no routes
  if (!router) {
 debug('no routes defined on app');
    done();
 return;
  }

  router.handle(req, res, done);
};

router.handle主要做两件事情:1. 根据传进来的参数req,做一些处理,如获取ur;2.遍历layer,这个是通过next函数来实现。

router.handle

next函数:按索引遍历Router.stack,找到匹配路径的中间件,然后执行layer.handle_request(req, res, next)

router_next

handle_request函数其实就是执行创建layer对象时传递的第三个参数(handle)。

  • 路由中间件的layer绑定的handle为route.dispatch

  • 非路由中间件的layer绑定的handle为自定义函数

layer.prototype.handle_request = function(req, res, next) {
  var fn = this.handle;
  if (fn.length > 3) return next(); // 如果参数不对,跳过本处理函数
  try {
    fn(req, res, next); // 执行处理函数
  } catch(err) {
    next(err);
  }
}

再来看route.dispatch: dispatch的功能就是,遍历route的stack数组,若layer的method和本次req的method匹配就执行。

// dispatch req, res into this route
Route.prototype.dispatch = function(req, res, next) {
  if (stack.length === 0) return;
  // ...不必要细节
  function next(err) {
    var layer = stack[idx++];
    // method就是http方法
    if (layer.method && layer.method !== method) { // 方法不匹配,跳过本处理函数
      return next(err);
    }
    layer.handle_request(req, res, next); // 执行处理函数
  }
  next()
}
总结

请求处理流程如图:

processing

  1. app监听端口,有request时,交给router处理。
  2. router处理步骤:
    1. 遍历其layer数组,找到匹配的路径的layer,根据不同类型layer做出不同的操作
      1. 路由中间件:执行匹配的layer然后将控制权移交Route,遍历Route的layer数组,执行完与request.method方法匹配的所有layer;最后控制权交还给Router
      2. 非路由中间件:执行匹配的layer

优/缺点

优点

  • 易上手: express对web开发相关的模块进行了适度的封装,屏蔽了大量复杂繁琐的技术细节,让开发者只需要专注于业务逻辑的开发,极大的降低了入门和学习的成本。
  • 高性能: express仅在web应用相关的Node.js模块上进行了适度的封装和扩展,较大程度避免了过度封装导致的性能损耗。
  • 扩展性强:**基于中间件的开发模式,使得express应用的扩展、模块拆分非常简单,既灵活,扩展性又强

缺点

  • 基于callback组合业务逻辑,业务逻辑复杂时嵌套过多,回调地狱蛋疼;
  • 没有统一的错误处理机制,error-first贯穿整个应用
  • Express团队维护频率低(推出了Koa)

总结

作为Node.js平台第一个Web应用框架,也是多数JavaScripter接触的第一个服务端框架,Express已经完成了它的历史使命。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.