仅愚

仅愚 查看完整档案

南京编辑安徽财经大学  |  工商管理 编辑口袋购物  |  前端设计师 前端开发工程师 编辑 github.com/shijinyu 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

仅愚 关注了收藏夹 · 2018-06-09

Node.js 优秀文章

SegmentFault 2015 Top Rank 系列之《Node.js 相关优秀文章》,该系列全部内容请看 http://segmentfault.com/bookmark/1230000004292712

关注 541

仅愚 关注了标签 · 2018-05-25

关注 193

仅愚 赞了文章 · 2018-04-26

前端路由的前生今世及实现原理

??

原文发于我的博客:https://github.com/hwen/blogS...

什么是路由

路由这个概念最先是后端出现的。在以前用模板引擎开发页面时,经常会看到这样

http://hometown.xxx.edu.cn/bbs/forum.php

有时还会有带.asp.html的路径,这就是所谓的SSR(Server Side Render),通过服务端渲染,直接返回页面。

其响应过程是这样的

1.浏览器发出请求

2.服务器监听到80端口(或443)有请求过来,并解析url路径

3.根据服务器的路由配置,返回相应信息(可以是 html 字串,也可以是 json 数据,图片等)

4.浏览器根据数据包的Content-Type来决定如何解析数据

简单来说路由就是用来跟后端服务器进行交互的一种方式,通过不同的路径,来请求不同的资源,请求不同的页面是路由的其中一种功能。

前端路由的诞生的缘由

前端路由的出现要从 ajax 开始,为什么?且听下面分析 (ˉ▽ ̄~)

Ajax,全称 Asynchronous JavaScript And XML,是浏览器用来实现异步加载的一种技术方案。在 90s 年代初,大多数的网页都是通过直接返回 HTML 的,用户的每次更新操作都需要重新刷新页面。及其影响交互体验,随着网络的发展,迫切需要一种方案来改善这种情况。

1996,微软首先提出 iframe 标签,iframe 带来了异步加载和请求元素的概念,随后在 1998 年,微软的 Outloook Web App 团队提出 Ajax 的基本概念(XMLHttpRequest的前身),并在 IE5 通过 ActiveX 来实现了这项技术。在微软实现这个概念后,其他浏览器比如 Mozilia,Safari,Opera 相继以 XMLHttpRequest 来实现 Ajax。(? 兼容问题从此出现,话说微软命名真喜欢用X,MFC源码一大堆。。)不过在 IE7 发布时,微软选择了妥协,兼容了 XMLHttpRequest 的实现。

有了 Ajax 后,用户交互就不用每次都刷新页面,体验带来了极大的提升。

但真正让这项技术发扬光大的,(。・∀・)ノ゙还是后来的 Google Map,它的出现向人们展现了 Ajax 的真正魅力,释放了众多开发人员的想象力,让其不仅仅局限于简单的数据和页面交互,为后来异步交互体验方式的繁荣发展带来了根基。

而异步交互体验的更高级版本就是 SPA(那么问个问题,异步交互最高级的体验是什么?会在文末揭晓)—— 单页应用。单页应用不仅仅是在页面交互是无刷新的,连页面跳转都是无刷新的,为了实现单页应用,所以就有了前端路由。

单页应用的概念是伴随着 MVVM 出现的。最早由微软提出,然后他们在浏览器端用 Knockoutjs 实现。但这项技术的强大之处并未当时的开发者体会到,可能是因为 Knockoutjs 实现过于复杂,导致没有大面积的扩散。

同样,这次接力的选手依然是 Google。Google 通过 Angularjs 将 MVVM 及单页应用发扬光大,让前端开发者能够开发出更加大型的应用,职能变得更大了。(不得不感慨,微软 跟 Google 都是伟大的公司)。随后都是大家都知道的故事,前端圈开始得到了爆发式的发展,陆续出现了很多优秀的框架。

本来还想写更多的。。。不过有点慢慢偏离主题了(下次会专门写写)

从 vue-router 来看前端路由实现原理

前端路由的实现其实很简单。

本质上就是检测 url 的变化,截获 url 地址,然后解析来匹配路由规则。

但是这样有人就会问:url 每次变化都会刷新页面啊?页面都刷新了,JavaScript 怎么检测和截获 url?

在 2014 年之前,大家是通过 hash 来实现路由,url hash 就是类似于

https://segmentfault.com/a/1190000011956628#articleHeader2

这种 #。后面 hash 值的变化,并不会导致浏览器向服务器发出请求,浏览器不发出请求,也就不会刷新页面。另外每次 hash 值的变化,还会触发 hashchange 这个事件,通过这个事件我们就可以知道 hash 值发生了哪些变化。

让我们来整理思路,假如我们要用 hash 的模式实现一个路由,那么流程应该是这样的。

hash-mode

hash 的实现相对来说要简单方便些,而且不用服务器来支持。

另外我们可以参考参考 vue-router 这一部分的实现(为了便于解释我简化了部分代码)

vue-router hash 实现源码地址

/**
 * 添加 url hash 变化的监听器
 */
setupListeners () {
  const router = this.router

  /**
   * 每当 hash 变化时就解析路径
   * 匹配路由
   */
  window.addEventListener('hashchange', () => {
    const current = this.current
    /**
     * transitionTo: 
     * 匹配路由
     * 并通过路由配置,把新的页面 render 到 ui-view 的节点
     */
    this.transitionTo(getHash(), route => {
      replaceHash(route.fullPath)
    })
  })
}

检测到 hash 的变化后,就可以通过替换 DOM 的方式来实现页面的更换。

14年后,因为HTML5标准发布。多了两个 API,pushStatereplaceState,通过这两个 API 可以改变 url 地址且不会发送请求。同时还有 onpopstate 事件。通过这些就能用另一种方式来实现前端路由了,但原理都是跟 hash 实现相同的。用了 HTML5 的实现,单页路由的 url 就不会多出一个#,变得更加美观。但因为没有 # 号,所以当用户刷新页面之类的操作时,浏览器还是会给服务器发送请求。为了避免出现这种情况,所以这个实现需要服务器的支持,需要把所有路由都重定向到根页面。具体可以见:HTML5 histroy 模式

同样,我们来理清下思路,这样写起代码才更得心应手~

html5-mode

这部分 vue-router 的源码,可以发现实现的思路大体也是相同的

export class HTML5History extends History {
  constructor (router, base) {
    super(router, base)
    /**
     * 原理还是跟 hash 实现一样
     * 通过监听 popstate 事件
     * 匹配路由,然后更新页面 DOM
     */
    window.addEventListener('popstate', e => {
      const current = this.current

      // Avoiding first `popstate` event dispatched in some browsers but first
      // history route not updated since async guard at the same time.
      const location = getLocation(this.base)
      if (this.current === START && location === initLocation) {
        return
      }

      this.transitionTo(location, route => {
        if (supportsScroll) {
          handleScroll(router, route, current, true)
        }
      })
    })
  }

  go (n) {
    window.history.go(n)
  }

  push (location, onComplete, onAbort) {
    const { current: fromRoute } = this
    this.transitionTo(location, route => {
      // 使用 pushState 更新 url,不会导致浏览器发送请求,从而不会刷新页面
      pushState(cleanPath(this.base + route.fullPath))
      onComplete && onComplete(route)
    }, onAbort)
  }

  replace (location, onComplete, onAbort) {
    const { current: fromRoute } = this
    this.transitionTo(location, route => {
      // replaceState 跟 pushState 的区别在于,不会记录到历史栈
      replaceState(cleanPath(this.base + route.fullPath))
      onComplete && onComplete(route)
    }, onAbort)
  }
}

造个轮子~

我用纯 ES6 写了个前端的路由,没有用任何框架,就可以实现前端路由的所有功能~

当然,实现的思路就是上面的那两幅流程图。

在线演示地址

项目源码

你还可以通过 npm 来安装这个包(功能已经实现完整,并有详细文档)

npm i --save sme-router

觉得有帮助的,请点个 star ~~

后记

异步交互的最高级体验是什么?
PWA,让前端页面可以做到离线操作(是不是越来越像原生 app 了?)

觉得有帮助的话,请点个赞,点个 star ~~

希望看到更多干货的话,(●'◡'●) 快来关注我的杂货铺吧
?

查看原文

赞 89 收藏 228 评论 18

仅愚 关注了专栏 · 2018-04-10

西厂 XUX

来自 阿里巴巴信息平台用户体验团队 的小伙伴们,分享前端设计方面的有趣知识。

关注 115

仅愚 关注了用户 · 2018-04-10

Poling @poling

动漫迷,前端菜鸟

关注 80

仅愚 赞了文章 · 2018-04-10

Electron 桌面应用开发系列文章 - 减小应用的打包体积

前言

笔者最近一直在使用 electron 开发一个可视化工具 Nowa,里面的技术栈是

  • webpack2

  • babili

  • react

  • electron

  • electron-builder

使用过 electron 的人都知道,打出来的包是很大的,因为electron 内置了 Node & Chromium, 所以啥都还没干,打出来的应用安装包就有几十兆了。

无法在 electron 上做文章,那么只好在 webpack 打包程序代码的过程中捣鼓了。以前打包应用的时候,程序里会有 node_modules 文件夹。这次任务就是干掉这个文件夹。

目录结构

ele0

大家会发现这里居然有两个 package.json !! 其实主要是因为 electron-builder 的 Two package.json Structure 的设置。把打包需要的依赖与开发依赖完全分开,纯粹打包你想要的东西,所以设置了 app 文件夹放这些。

electron-builder 只会对 app 文件夹进行打包,换句话说,这里面有多少东西就会打包多少内容。

所以我们可以想法设法减少不必要的东西。比如这里没有任何依赖, node_modules 是空的!

打包过程干货

结合 electron 的特殊环境,webpack 编译过程有很多文章可以做。

1、 考虑 electron 的 Chromium & Node 版本

在 webpack 打包的时候,我们抛弃低版本浏览器的那些兼容,因为我们只用 Chromium,所以不必要的会增加编译输出的 preset 就不要了,比如 loose,和一些 shim。

而且高版本的 node 已经支持一些 es6 的属性了,我们真的需要降级到 es5 么?当然不是的。

A、 修改 babel 配置

推荐使用 babel-preset-env设置。这个 preset 主要可以设置项目当前的环境,适时进行引入新特性,如果对其没有其他配置,就相当于使用了 babel-preset-latest

// .babelrc

{
  "presets": [
    ["env", {
      "targets": {
        "electron": 1.6,
      },
      "loose": false, 
      "modules": false,
      "useBuiltIns": true
    }],
    "stage-2",
    "react"
  ],
  "plugins": [
  ]
}

demo 里面设置了 targets 是 electron 1.6 版本,如果不嫌麻烦的话,可以根据当前的 electron 的 node 和 chrome 版本进行分别设置。
比如:


"targets": {
    "node": 7.4,
    "chrome": 56
}

B、 更换压缩方案

通常我们以前的打包方案是这样子的:

ES2015+ code -> Babel -> Babili/Uglify -> Minified ES5 Code

现在,我们可以不用降级这么多,使用一个工具babili(不要看成bilibili),它是 babel 的压缩工具。

babili 的打包方案是这样的:

ES2015+ code -> Babili -> Minified ES2015+ Code

它不会编译成 es5 的版本,而是对当前版本进行压缩。这简直就是 electron 的绝配啊。

为了能在 webpack 中使用,我们需要引入一个插件 babili-webpack-plugin。 这个是使用于生产环境的,所以我们 webpack 生产环境配置中可以这样引入:

// webpack.prod.config.js

const BabiliPlugin = require('babili-webpack-plugin');

module.exports = {
    ...,
    
    plugins: [
        ...,
        new BabiliPlugin()
    ]
}

2、 对 main 端代码进行打包

通常我们可能不对 main 端进行打包,我之前做的项目就没打包,main 端的依赖全部都合入安装包去了。如果 main 端依赖很大的话,那真是灾难。

实际上 main 端也能进行打包,与 renderer 端一样,输出到 app 目录,这样 node_modules 就空了。

然而,如果有引入第三方的 native node 模块的话,笔者没有尝试过是否能行得通,猜测很可能还是要放到 node_modules 里面保险。有尝试过的看官请留言。

对 main & renderer 端打包代码的时候,要注意设置 webpack 的 target 字段。

// renderer.webpack.config

{
    target: 'electron-renderer'
}

// main.webpack.config

{
    target: 'electron-main'
}

webpack 的 target 默认是 web。如果你没有进行更改的话,renderer 端就无法使用 node 模块了。

main 端注意事项

对 main 端打包的条件是有些条件的。

  • 如果说您使用了remote.require(xxx) 的方式在 renderer 端引入了 main 端需要的模块,那么您需要在 app 目录下放该模块。

  • 如果在 main 端调用了 child_process 的方法去执行放在 app 文件夹里面的js文件,而这些脚本依赖了非 node 原生模块的时候,请把这些模块安装到 app 里面的 node_modules 里面。

main 端遇到的问题

main 端打包容易碰到如下问题:

依赖中出现 #!/usr/bin/env node 这样的语句或者包含了 *.node 的脚本,这个使用您需要使用一些特殊的 loader 进行处理。

{
 test: /\.js$/,
 include: /node_modules/,
 loader: 'shebang-loader'
},
{
   test: /\.node$/,
   include: /node_modules/,
   loader: 'node-loader'
}

3、合适的renderer 端构建方案

笔者在renderer 端构建采用了 DLL(动态链接库)方案, 也是 webpack 官方比较推荐的方案。它可以快速的提升构建速度,特别是明显的提升第一次启动的速度。在生产环境就不要使用它了,因为 dll 文件的体积比较大。
css 要使用 ExtractTextPlugin 与 js 代码分离开来,不要合并,不要合并,因为文件体积同样比较大。

4、 注意 electron 版本号 和 electron-builder 版本号

使用新的 electron 版本打包出来的安装包会比旧版本大几兆,其实很容易理解。

使用不同版本的 electron-builder 打包出来的也不同。大于 13.* 版本的打包出来的安装包同样大几兆。

几兆到底是几兆呢? demo 的例子实测是 3~5 MB。如果大家不care这几兆的话其实无所谓。

为了减小安装包体积,笔者真是无所不用其极。

如果大家有更好的打包方式,请评论回复。

参考

查看原文

赞 9 收藏 25 评论 7

仅愚 收藏了问题 · 2018-04-04

当在Webpack里配置了别名之后,Webstorm里可以设置路径提示吗

比如我像下面这样设置了别名之后

图片描述

我在代码里引入文件的时候
图片描述

能正确的引用到文件,但是在打代码的时候他没有路径提示了。
而且这个波浪线看着也很不舒服。

请问webstorm可以根据别名设置路径提示吗

仅愚 关注了问题 · 2018-04-04

解决当在Webpack里配置了别名之后,Webstorm里可以设置路径提示吗

比如我像下面这样设置了别名之后

图片描述

我在代码里引入文件的时候
图片描述

能正确的引用到文件,但是在打代码的时候他没有路径提示了。
而且这个波浪线看着也很不舒服。

请问webstorm可以根据别名设置路径提示吗

关注 5 回答 3

仅愚 赞了问题 · 2018-04-04

解决当在Webpack里配置了别名之后,Webstorm里可以设置路径提示吗

比如我像下面这样设置了别名之后

图片描述

我在代码里引入文件的时候
图片描述

能正确的引用到文件,但是在打代码的时候他没有路径提示了。
而且这个波浪线看着也很不舒服。

请问webstorm可以根据别名设置路径提示吗

关注 5 回答 3

认证与成就

  • 获得 14 次点赞
  • 获得 14 枚徽章 获得 0 枚金徽章, 获得 5 枚银徽章, 获得 9 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

  • Skit : a sass framework

    SKit的目标是提供一套规范而可配置的sassy css kit。SKit提供一套类似BEM的class样式命名规范。SKit的所有样式组件都是可更改、可添加、可卸载的。SKit提供可配置预设的变量,您可以根据需求自行修改字体、字号、配色。Skit具有灵活地耦合性。

注册于 2012-11-13
个人主页被 520 人浏览