须臾

须臾 查看完整档案

北京编辑太原科技大学  |  数学与应用数学 编辑  |  填写所在公司/组织 tohavelighting.github.io/ 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

须臾 回答了问题 · 2020-08-26

vue 在methods里面从后台获取数据之后赋值,在其他方法中不能读取这个值

FetchData中的GetQuestionInfo是一个异步的方法,它的结果还没有返回的时候,newQsInfo这个方法已经开始执行了

关注 3 回答 2

须臾 关注了问题 · 2020-07-21

谷歌浏览器启动后得页面被一个不知名得网站劫持,然后跳转到hao123

就这个http://ljd1.gndh666.top/,每次...,然后就跳转到了hao123.com,尝试各种方法无法解决这个问题

关注 8 回答 6

须臾 收藏了文章 · 2020-05-19

现代富文本编辑器Quill的内容渲染机制

DevUI是一支兼具设计视角和工程视角的团队,服务于华为云DevCloud平台和华为内部数个中后台系统,服务于设计师和前端工程师。
官方网站:devui.design
Ng组件库:ng-devui(欢迎Star)

引言

在 Web 开发领域,富文本编辑器( Rich Text Editor )是一个使用场景非常广,又非常复杂的组件。

要从0开始做一款好用、功能强大的富文本编辑器并不容易,基于现有的开源库进行开发能节省不少成本。

Quill 是一个很不错的选择。

本文主要介绍Quill内容渲染相关的基本原理,主要包括:

  1. Quill描述编辑器内容的方式
  2. Quill将Delta渲染到DOM的基本原理
  3. Scroll类管理所有子Blot的基本原理

Quill如何描述编辑器内容?

Quill简介

Quill 是一款API驱动、易于扩展和跨平台的现代 Web 富文本编辑器。目前在 Github 的 star 数已经超过25k。

Quill 使用起来也非常方便,简单几行代码就可以创建一个基本的编辑器:

<script>
  var quill = new Quill('#editor', {
    theme: 'snow'
  });
</script>

Quill如何描述格式化的文本

当我们在编辑器里面插入一些格式化的内容时,传统的做法是直接往编辑器里面插入相应的 DOM,通过比较 DOM 树来记录内容的改变。

直接操作 DOM 的方式有很多不便,比如很难知道编辑器里面某些字符或者内容到底是什么格式,特别是对于自定义的富文本格式。

Quill 在 DOM 之上做了一层抽象,使用一种非常简洁的数据结构来描述编辑器的内容及其变化:Delta。

Delta 是JSON的一个子集,只包含一个 ops 属性,它的值是一个对象数组,每个数组项代表对编辑器的一个操作(以编辑器初始状态为空为基准)。

比如编辑器里面有"HelloWorld":

用 Delta 进行描述如下:

{
  "ops": [
    { "insert": "Hello " },
    { "insert": "World", "attributes": { "bold": true } },
    { "insert": "\n" }
  ]
}

意思很明显,在空的编辑器里面插入"Hello ",在上一个操作后面插入加粗的"World",最后插入一个换行"n"。

Quill如何描述内容的变化

Delta 非常简洁,但却极富表现力。

它只有3种动作和1种属性,却足以描述任何富文本内容和任意内容的变化。

3种动作:

  • insert:插入
  • retain:保留
  • delete:删除

1种属性:

  • attributes:格式属性

比如我们把加粗的"World"改成红色的文字"World",这个动作用 Delta 描述如下:

{
  "ops": [
    { "retain": 6 },
    { "retain": 5, "attributes": { "color": "#ff0000" } }
  ]
}

意思是:保留编辑器最前面的6个字符,即保留"Hello "不动,保留之后的5个字符"World",并将这些字符设置为字体颜色为"#ff0000"。

如果要删除"World",相信聪明的你也能猜到怎么用 Delta 描述,没错就是你猜到的:

{
  "ops": [
    { "retain": 6 },
    { "delete": 5 }
  ]
}

Quill如何描述富文本内容

最常见的富文本内容就是图片,Quill 怎么用 Delta 描述图片呢?

insert 属性除了可以是用于描述普通字符的字符串格式之外,还可以是描述富文本内容的对象格式,比如图片:

{
  "ops": [
    { "insert": { "image": "https://quilljs.com/assets/images/logo.svg" } },
    { "insert": "\n" }
  ]
}

比如公式:

{ 
  "ops": [ 
    { "insert": { "formula": "e=mc^2" } }, 
    { "insert": "\n" } 
  ]
}

Quill 提供了极大的灵活性和可扩展性,可以自由定制富文本内容和格式,比如幻灯片、思维导图,甚至是3D模型。

setContent如何将Delta数据渲染成DOM?

上一节我们介绍了 Quill 如何使用 Delta 描述编辑器内容及其变化,我们了解到 Delta 只是普通的 JSON 结构,只有3种动作和1种属性,却极富表现力。

那么 Quill 是如何应用 Delta 数据,并将其渲染到编辑器中的呢?

setContents 初探

Quill 中有一个 API 叫 setContents,可以将 Delta 数据渲染到编辑器中,本期将重点解析这个 API 的实现原理。

还是用上一期的 Delta 数据作为例子:

const delta = {  "ops": [
    { "insert": "Hello " },
    { "insert": "World", "attributes": { "bold": true } },
    { "insert": "\n" } ]
}

当使用 new Quill() 创建好 Quill 的实例之后,我们就可以调用它的 API 啦。

const quill = new Quill('#editor', {
  theme: 'snow'
});

我们试着调用下 setContents 方法,传入刚才的 Delta 数据:

quill.setContents(delta);

编辑器中就出现了我们预期的格式化文本:

setContents 源码

通过查看 setContents 的源码,发现就调用了 modify 方法,主要传入了一个函数:

setContents(delta, source = Emitter.sources.API) {
  return modify.call( this, () => {
    delta = new Delta(delta);
    const length = this.getLength();
    const deleted = this.editor.deleteText(0, length);
    const applied = this.editor.applyDelta(delta);
    ... // 为了方便阅读,省略了非核心代码
    return deleted.compose(applied);
  }, source, );
}

使用 call 方法调用 modify 是为了改变其内部的 this 指向,这里指向的是当前的 Quill 实例,因为 modify 方法并不是定义在 Quill 类中的,所以需要这么做。

我们先不看 modify 方法,来看下传入 modify 方法的匿名函数。

该函数主要做了三件事:

  1. 把编辑器里面原有的内容全部删除
  2. 应用传入的 Delta 数据,将其渲染到编辑器中
  3. 返回1和2组合之后的 Delta 数据

我们重点看第2步,这里涉及到 Editor 类的 applyDelta 方法。

applyDelta 方法解析

根据名字大概能猜到该方法的目的是:把传入的 Delta 数据应用和渲染到编辑器中。

它的实现我们大概也可以猜测就是:循环 Delta 里的 ops 数组,一个一个地应用到编辑器中。

它的源码一共54行,大致如下:

applyDelta(delta) {
  let consumeNextNewline = false;
  this.scroll.update();
  let scrollLength = this.scroll.length();
  this.scroll.batchStart();
  const normalizedDelta = normalizeDelta(delta);

  normalizedDelta.reduce((index, op) => {
    const length = op.retain || op.delete || op.insert.length || 1;
    let attributes = op.attributes || {};    
    // 1.插入文本
    if (op.insert != null) {
      if (typeof op.insert === 'string') {        
        // 普通文本内容
        let text = op.insert;        
        ... // 为了阅读方便,省略非核心代码
        this.scroll.insertAt(index, text);
        ... // 为了阅读方便,省略非核心代码
      } else if (typeof op.insert === 'object') {
        // 富文本内容
        const key = Object.keys(op.insert)[0];
        // There should only be one key
        if (key == null) return index;
        this.scroll.insertAt(index, key, op.insert[key]);
      }
      scrollLength += length;
    }    
    // 2.对文本进行格式化
    Object.keys(attributes).forEach(name => {
      this.scroll.formatAt(index, length, name, attributes[name]);
    });
    return index + length;
  }, 0);
... // 为了阅读方便,省略非核心代码  this.scroll.batchEnd();
  this.scroll.optimize();
  return this.update(normalizedDelta);
}

和我们猜测的一样,该方法就是用 Delta 的 reduce 方法对传入的 Delta 数据进行迭代,将插入内容和删除内容的逻辑分开了,插入内容的迭代里主要做了两件事:

  1. 插入普通文本或富文本内容:insertAt
  2. 格式化该文本:formatAt

至此,将 Delta 数据应用和渲染到编辑器中的逻辑,我们已经解析完毕。
下面做一个总结:

  1. setContents 方法本身没有什么逻辑,仅仅是调用了 modify 方法而已
  2. 在传入 modify 方法的匿名函数中调用了 Editor 对象的 applyDelta 方法
  3. applyDelta 方法对传入的 Delta 数据进行迭代,并依次插入/格式化/删除 Delta 数据所描述的编辑器内容

Scroll如何管理所有的Blot类型?

上一节我们介绍了 Quill 将 Delta 数据应用和渲染到编辑器中的原理:通过迭代 Delta 中的 ops 数据,将 Delta 行一个一个渲染到编辑器中。

了解到最终内容的插入和格式化都是通过调用 Scroll 对象的方法实现的,Scroll 对象到底是何方神圣?在编辑器的操作中发挥了什么作用?

Scroll 对象的创建‍

上一节的解析终止于 applyDelta 方法,该方法最终调用了 this.scroll.insertAt 将 Delta 内容插入到编辑器中。

applyDelta 方法定义在 Editor 类中,在 Quill 类的 setContents 方法中被调用,通过查看源码,发现 this.scroll 最初是在 Quill 的构造函数中被赋值的。

this.scroll = Parchment.create(this.root, {
  emitter: this.emitter,
  whitelist: this.options.formats
});

Scroll 对象是通过调用 Parchment 的 create 方法创建的。

前面两期我们简单介绍了 Quill 的数据模型 Delta,那么 Parchment 又是什么呢?它跟 Quill 和 Delta 是什么关系?这些疑问我们先不解答,留着后续详细讲解。

先来简单看下 create 方法是怎么创建 Scroll 对象的,create 方法最终是定义在 parchment 库源码中的 registry.ts 文件中的,就是一个普通的方法:

export function create(input: Node | string | Scope, value?: any): Blot {
  // 传入的 input 就是编辑器主体 DOM 元素(.ql-editor),里面包含了编辑器里所有可编辑的实际内容   
  // match 是通过 query 方法查询到的 Blot 类,这里就是 Scroll 类  
  let match = query(input);
  if (match == null) {
    throw new ParchmentError(`Unable to create ${input} blot`);
  }  
  let BlotClass = <BlotConstructor>match;  
  let node = input instanceof Node || input['nodeType'] === Node.TEXT_NODE
    ? input
    : BlotClass.create(value);

  // 最后返回 Scroll 对象
  return new BlotClass(<Node>node, value);
}

create 方法的入参是编辑器主体 DOM 元素 .ql-editor,通过调用同文件中的 query 普通方法,查询到 Blot 类是 Scroll 类,查询的大致逻辑就是在一个 map 表里查,最后通过 new Scroll() 返回 Scroll 对象实例,赋值给 this.scroll。

{
  ql-cursor: ƒ Cursor(domNode, selection),  
  ql-editor: ƒ Scroll(domNode, config), // 这个就是 Scroll 类
  ql-formula: ƒ FormulaBlot(),
  ql-syntax: ƒ SyntaxCodeBlock(),
  ql-video: ƒ Video(),
}

Scroll 类详解

Scroll 类是我们解析的第一个 Blot 格式,后续我们将遇到各种形式的 Blot 格式,并且会定义自己的 Blot 格式,用于在编辑器中插入自定义内容,这些 Blot 格式都有类似的结构。

可以简单理解为 Blot 格式是对 DOM 节点的抽象,而 Parchment 是对 HTML 文档的抽象,就像 DOM 节点是构成 HTML 文档的基本单元一样,Blot 是构成 Parchment 文档的基本单元。

比如:DOM 节点是<div>,对其进行封装变成 <div class="ql-editor">,并在其内部封装一些属性和方法,就变成 Scroll 类。

Scroll 类是所有 Blot 的根 Blot,它对应的 DOM 节点也是编辑器内容的最外层节点,所有编辑器内容都被包裹在它之下,可以认为 Scroll 统筹着其他 Blot 对象(实际 Scroll 的父类 ContainerBlot 才是幕后总 BOSS,负责总的调度)。

<div class="ql-editor" contenteditable="true">
  <p>
    Hello
    <strong>World</strong>
  </p>
  ... // 其他编辑器内容
</div>

Scroll 类定义在 Quill 源码中的 blots/scroll.js 文件中,之前 applyDelta 方法中通过 this.scroll 调用的 insertAt / formatAt / deleteAt / update / batchStart / batchEnd / optimize 等方法都在 Scroll 类中。

以下是 Scroll 类的定义:

class Scroll extends ScrollBlot {
  constructor(domNode, config) {
    super(domNode);
    ...  
  }    

  // 标识批量更新的开始,此时执行 update / optimize 都不会进行实际的更新   
  batchStart() {
    this.batch = true;  
  }    

  // 标识批量更新的结束
  batchEnd() {
    this.batch = false;
    this.optimize();  
  }    

  // 在制定位置删除制定长度的内容  
  // 比如:deleteAt(6, 5) 将删除 "World"  
  // 在 Quill 的 API 中对应 deleteText(index, length, source) 方法  
  deleteAt(index, length) {}   
 
  // 设置编辑器的可编辑状态  
  enable(enabled = true) {
    this.domNode.setAttribute('contenteditable', enabled);  
  }    

  // 在制定位置用制定格式格式化制定长度的内容  
  // 比如:formatAt(6, 5, 'bold', false) 将取消 "World" 的粗体格式  
  // 在 Quill 的 API 中对应 formatText(index, length, name, value, source) 方法 formatAt(index, length,  format, value) {
    if (this.whitelist != null && !this.whitelist[format]) return;
    super.formatAt(index, length, format, value); this.optimize();  
  }    

  // 在制定位置插入内容  
  // 比如:insertAt(11, '\n你好,世界');  
  // 在 Quill 的 API 中对应 insertText(index, text, name, value, source)  
  // Quill 中的 insertText 其实是 Scroll 的 insertAt 和 formatAt 的复合方法  
  insertAt(index, value, def) {}    

  // 在某个 Blot 前面插入 Blot  
  insertBefore(blot, ref) {}    

  // 弹出当前位置 Blot 路径最外面的叶子 Blot(会改变原数组)
  leaf(index) { return this.path(index).pop() || [null, -1];  }    

  // 实际上调用的是父类 ContainerBlot 的 descendant 方法  
  // 目的是得到当前位置所在的 Blot 对象
  line(index) {
    if (index === this.length()) {
      return this.line(index - 1);
    }
    return this.descendant(isLine, index);
  }    

  // 获取某一范围的 Blot 对象  
  lines(index = 0, length = Number.MAX_VALUE) {}    

  // TODO
  optimize(mutations = [], context = {}) {
    if (this.batch === true) return;
    super.optimize(mutations, context);
    if (mutations.length > 0) {
      this.emitter.emit(Emitter.events.SCROLL_OPTIMIZE, mutations, context);
    }  
  }    

  // 实际上调用的是父类 ContainerBlot 的 path 方法  
  // 目的是得到当前位置的 Blot 路径,并排除 Scroll 自己  
  // Blot 路径就和 DOM 节点路径是对应的  
  // 比如:DOM 节点路径 div.ql-editor -> p -> strong,  
  // 对应 Blot 路径就是 [[Scroll div.ql-editor, 0], [Block p, 0], [Bold strong, 6]]
  path(index) {
    return super.path(index).slice(1); // Exclude self  
  }    

  // TODO
  update(mutations) {
    if (this.batch === true) return;
    ...  
  }
}

Scroll.blotName = 'scroll';
Scroll.className = 'ql-editor';
Scroll.tagName = 'DIV';
Scroll.defaultChild = 'block';
Scroll.allowedChildren = [Block, BlockEmbed, Container];

export default Scroll;

Scroll 类上定义的静态属性 blotName 和 tagName 是必须的,前者用于唯一标识该 Blot 格式,后者对应于一个具体的 DOM 标签,一般还会定义一个 className,如果该 Blot 是一个父级 Blot,一般还会定义 allowedChildren 用来限制允许的子级 Blot 白名单,不在白名单之内的子级 Blot 对应的 DOM 将无法插入父类 Blot 对应的 DOM 结构里。

Scroll 类中除了定义了插入 / 格式化 / 删除内容的方法之外,定义了一些很实用的用于获取当前位置 Blot 路径和 Blot 对象的方法,以及触发编辑器内容更新的事件。

相应方法的解析都在以上源码的注释里,其中 optimize 和 update 方法涉及 Quill 中的事件和状态变更相关逻辑,放在后续单独进行解析。

关于 Blot 格式的规格定义文档可以参阅以下文章:

https://github.com/quilljs/parchment#blots

我也是初次使用Quill进行富文本编辑器的开发,难免有理解不到位的地方,欢迎大家提意见和建议。

加入我们

我们是DevUI团队,欢迎来这里和我们一起打造优雅高效的人机设计/研发体系。招聘邮箱:muyang2@huawei.com。

文/DevUI Kagol

往期文章推荐

查看原文

须臾 收藏了文章 · 2020-05-11

搭建一个vue-cli4+webpack移动端框架(开箱即用)

简介

这是基于 vue-cli4 实现的移动端框架,其中包含项目常用的配置,组件封装及webpack优化方法,可供快速开发使用。

技术栈:vue-cli4 + webpack4 + vant + axios + less + postcss-px2rem

源码 github.com/Michael-lzg…

// 安装依赖
npm install

// 本地启动
npm run dev

// 生产打包
npm run build

在一两年前,vue-cli3已经声驾到3.0+版本,但是由于旧项目一致习惯于vue-cli2的脚手架的使用,之前也写过一篇 搭建一个vue-cli的移动端H5开发模板 简单总结了一点移动端的开发技巧。

近日升级vue-cli脚手架才发现,这已经升级到4.0+版本了,觉得很多必要在新的项目中使用vue-cli4进行开发,加上近来对webpack有了进一步理解,所以结合了vue-cli4和webpack搭建了一个移动端框架,以便开箱即用。 主要包括如下技术点:

  • vue-cli4脚手架
  • vant按需引入
  • 移动端rem适配
  • axios拦截封装
  • util工具类函数封装
  • vue-router配置
  • 登录权限校验
  • 多环境变量配置
  • vue.config.js配置
  • toast组件封装
  • dialog组件封装
  • 跨域代理设置
  • webpack打包可视化分析
  • CDN资源优化
  • gzip打包优化
  • 首页添加骨架屏

关于更多的webpack优化方法,可参考 github.com/Michael-lzg…

配置 vant

vant 是一套轻量、可靠的移动端 Vue 组件库,非常适合基于 vue 技术栈的移动端开发。在过去很长的一段时间内,本人用的移动端 UI 框架都是 vux。后来由于 vux 不支持 vue-cli3,就转用了 vant,不得不说,无论是在交互体验上,还是代码逻辑上,vant 都比 vux 好很多,而且 vant 的坑比较少。

对于第三方 UI 组件,如果是全部引入的话,比如会造成打包体积过大,加载首页白屏时间过长的问题,所以按需加载非常必要。vant 也提供了按需加载的方法。babel-plugin-import 是一款 babel 插件,它会在编译过程中将 import 的写法自动转换为按需引入的方式。

1、安装依赖

npm i babel-plugin-import -D

2、配置 .babelrc 或者 babel.config.js 文件

// 在.babelrc 中添加配置
{
  "plugins": [
    ["import", {
      "libraryName": "vant",
      "libraryDirectory": "es",
      "style": true
    }]
  ]
}

// 对于使用 babel7 的用户,可以在 babel.config.js 中配置
module.exports = {
  plugins: [
    ['import', {
      libraryName: 'vant',
      libraryDirectory: 'es',
      style: true
    }, 'vant']
  ]
};

3、按需引入

你可以在代码中直接引入 Vant 组件,插件会自动将代码转化为方式二中的按需引入形式

import Vue from 'vue'
import { Button } from 'vant'

Vue.use(Button)

rem 适配

移动端适配是开发过程中不得不面对的事情。在此,我们使用 postcss 中的 px2rem-loader,将我们项目中的 px 按一定比例转化 rem,这样我们就可以对着蓝湖上的标注写 px 了。

我们将 html 字跟字体设置为 100px,很多人选择设置为 375px,但是我觉得这样换算出来的 rem 不够精确,而且我们在控制台上调试代码的时候无法很快地口算得出它本来的 px 值。如果设置 1rem=100px,这样我们看到的 0.16rem,0.3rem 就很快得算出原来是 16px,30px 了。

具体步骤如下;

1、安装依赖

npm install px2rem-loader --save-dev

2、在 vue.config.js 进行如下配置

  css: {
    // css预设器配置项
    loaderOptions: {
      postcss: {
        plugins: [
          require('postcss-px2rem')({
            remUnit: 100
          })
        ]
      }
    }
  },

3、在 main.js 设置 html 跟字体大小

function initRem() {
  let cale = window.screen.availWidth > 750 ? 2 : window.screen.availWidth / 375
  window.document.documentElement.style.fontSize = `${100 * cale}px`
}

window.addEventListener('resize', function() {
  initRem()
})

axios 请求封装

1、设置请求拦截和响应拦截

const PRODUCT_URL = 'https://xxxx.com'
const MOCK_URL = 'http://xxxx.com'
let http = axios.create({
  baseURL: process.env.NODE_ENV === 'production' ? PRODUCT_URL : MOCK_URL,
})
// 请求拦截器
http.interceptors.request.use(
  (config) => {
    // 设置token,Content-Type
    var token = sessionStorage.getItem('token')
    config.headers['token'] = token
    config.headers['Content-Type'] = 'application/json;charset=UTF-8'
    // 请求显示loading效果
    if (config.loading === true) {
      vm.$loading.show()
    }
    return config
  },
  (error) => {
    vm.$loading.hide()
    return Promise.reject(error)
  }
)
// 响应拦截器
http.interceptors.response.use(
  (res) => {
    vm.$loading.hide()
    // token失效,重新登录
    if (res.data.code === 401) {
      //  重新登录
    }
    return res
  },
  (error) => {
    vm.$loading.hide()
    return Promise.reject(error)
  }
)

2、封装 get 和 post 请求方法

function get(url, data, lodaing) {
  return new Promise((resolve, reject) => {
    http
      .get(url)
      .then(
        (response) => {
          resolve(response)
        },
        (err) => {
          reject(err)
        }
      )
      .catch((error) => {
        reject(error)
      })
  })
}

function post(url, data, loading) {
  return new Promise((resolve, reject) => {
    http
      .post(url, data, { loading: loading })
      .then(
        (response) => {
          resolve(response)
        },
        (err) => {
          reject(err)
        }
      )
      .catch((error) => {
        reject(error)
      })
  })
}

export { get, post }

3、把 get,post 方法挂载到 vue 实例上。

// main.js
import { get, post } from './js/ajax'
Vue.prototype.$http = { get, post }

工具类函数封装

1、添加方法到 vue 实例的原型链上

export default {
  install (Vue, options) {
    Vue.prototype.util = {
      method1(val) {
        ...
      },
      method2 (val) {
       ...
      },
  }
}

2、在 main.js 通过 vue.use()注册

import utils from './js/utils'
Vue.use(utils)

vue-router 配置

平时很多人对 vue-router 的配置可配置了 path 和 component,实现了路由跳转即可。其实 vue-router 可做的事情还有很多,比如

  • 路由懒加载配置
  • 改变单页面应用的 title
  • 登录权限校验
  • 页面缓存配置

路由懒加载配置

Vue 项目中实现路由按需加载(路由懒加载)的 3 中方式:

// 1、Vue异步组件技术:
{
  path: '/home',
  name: 'Home',
  component: resolve => reqire(['../views/Home.vue'], resolve)
}

// 2、es6提案的import()
{
  path: '/',
  name: 'home',
  component: () => import('../views/Home.vue')
}

// 3、webpack提供的require.ensure()
{
  path: '/home',
  name: 'Home',
  component: r => require.ensure([],() =>  r(require('../views/Home.vue')), 'home')
}

本项目采用的是第二种方式,为了后续 webpack 打包优化。

改变单页面应用的 title

由于单页面应用只有一个 html,所有页面的 title 默认是不会改变的,但是我们可以才路由配置中加入相关属性,再在路由守卫中通过 js 改变页面的 title

router.beforeEach((to, from, next) => {
  document.title = to.meta.title
})

登录权限校验

在应用中,通常会有以下的场景,比如商城:有些页面是不需要登录即可访问的,如首页,商品详情页等,都是用户在任何情况都能看到的;但是也有是需要登录后才能访问的,如个人中心,购物车等。此时就需要对页面访问进行控制了。

此外,像一些需要记录用户信息和登录状态的项目,也是需要做登录权限校验的,以防别有用心的人通过直接访问页面的 url 打开页面。

此时。路由守卫可以帮助我们做登录校验。具体如下:

1、配置路由的 meta 对象的 auth 属性

const routes = [
  {
    path: '/',
    name: 'home',
    component: () => import('../views/Home.vue'),
    meta: { title: '首页', keepAlive: false, auth: false },
  },
  {
    path: '/mine',
    name: 'mine',
    component: () => import('../views/mine.vue'),
    meta: { title: '我的', keepAlive: false, auth: true },
  },
]

2、在路由首页进行判断。当to.meta.authtrue(需要登录),且不存在登录信息缓存时,需要重定向去登录页面

router.beforeEach((to, from, next) => {
  document.title = to.meta.title
  const userInfo = sessionStorage.getItem('userInfo') || null
  if (!userInfo && to.meta.auth) {
    next('/login')
  } else {
    next()
  }
})

页面缓存配置

项目中,总有一些页面我们是希望加载一次就缓存下来的,此时就用到 keep-alive 了。keep-alive 是 Vue 提供的一个抽象组件,用来对组件进行缓存,从而节省性能,由于是一个抽象组件,所以在 v 页面渲染完毕后不会被渲染成一个 DOM 元素。

1、通过配置路由的 meta 对象的 keepAlive 属性值来区分页面是否需要缓存

const routes = [
  {
    path: '/',
    name: 'home',
    component: () => import('../views/Home.vue'),
    meta: { title: '首页', keepAlive: false, auth: false },
  },
  {
    path: '/list',
    name: 'list',
    component: () => import('../views/list.vue'),
    meta: { title: '列表页', keepAlive: true, auth: false },
  },
]

2、在 app.vue 做缓存判断

<div id="app">
  <router-view v-if="!$route.meta.keepAlive"></router-view>
  <keep-alive>
    <router-view v-if="$route.meta.keepAlive"></router-view>
  </keep-alive>
</div>

多环境变量配置

首先我们先来了解一下环境变量,一般情况下我们的项目会有三个环境,本地环境(development),测试环境(test),生产环境(production),我们可以在项目根目录下建三个配置环境变量的文件.env.development.env.test.env.production

环境变量文件中只包含环境变量的“键=值”对:

NODE_ENV = 'production'
VUE_APP_ENV = 'production' // 只有VUE_APP开头的环境变量可以在项目代码中直接使用

除了自定义的 VUE_APP_*变量之外,还有两个可用的变量:

  • NODE_ENV : "development"、"production" 或 "test"中的一个。具体的值取决于应用运行的模式。
  • BASE_URL : 和 vue.config.js 中的 publicPath 选项相符,即你的应用会部署到的基础路径。

下面开始配置我们的环境变量

1、在项目根目录中新建.env.*

  • .env.development 本地开发环境配置
NODE_ENV='development'
VUE_APP_ENV = 'development'
  • env.staging 测试环境配置
NODE_ENV='production'
VUE_APP_ENV = 'staging'
  • env.production 正式环境配置
NODE_ENV='production'
VUE_APP_ENV = 'production'

为了在不同环境配置更多的变量,我们在 src 文件下新建一个 config/index

// 根据环境引入不同配置 process.env.NODE_ENV
const config = require('./env.' + process.env.VUE_APP_ENV)
module.exports = config

在同级目录下新建 env.development.jsenv.test.jsenv.production.js,在里面配置需要的变量。
以 env.development.js 为例

module.exports = {
  baseUrl: 'http://localhost:8089', // 项目地址
  baseApi: 'https://www.mock.com/api', // 本地api请求地址
}

2、配置打包命令

package.json 里的 scripts 不同环境的打包命令

  • 通过 npm run serve 启动本地
  • 通过 npm run test 打包测试
  • 通过 npm run build 打包正式
"scripts": {
  "dev": "vue-cli-service serve",
  "build": "vue-cli-service build",
  "test": "vue-cli-service build --mode test",
}

vue.config.js 配置

vue-cli3 开始,新建的脚手架都需要我们在 vue.config.js 配置我们项目的东西。主要包括

  • 打包后文件输出位置
  • 关闭生产环境 souecemap
  • 配置 rem 转化 px
  • 配置 alias 别名
  • 去除生产环境 console
  • 跨域代理设置

此外,还有很多属于优化打包的配置,后面会一一道来。

module.exports = {
  publicPath: './', // 默认为'/'

  // 将构建好的文件输出到哪里,本司要求
  outputDir: 'dist/static',

  // 放置生成的静态资源(js、css、img、fonts)的目录。
  assetsDir: 'static',

  // 指定生成的 index.html 的输出路径
  indexPath: 'index.html',

  // 是否使用包含运行时编译器的 Vue 构建版本。
  runtimeCompiler: false,

  transpileDependencies: [],

  // 如果你不需要生产环境的 source map
  productionSourceMap: false,

  // 配置css
  css: {
    // 是否使用css分离插件 ExtractTextPlugin
    extract: true,
    sourceMap: true,
    // css预设器配置项
    loaderOptions: {
      postcss: {
        plugins: [
          require('postcss-px2rem')({
            remUnit: 100,
          }),
        ],
      },
    },
    // 启用 CSS modules for all css / pre-processor files.
    modules: false,
  },

  // 是一个函数,允许对内部的 webpack 配置进行更细粒度的修改。
  chainWebpack: (config) => {
    // 配置别名
    config.resolve.alias
      .set('@', resolve('src'))
      .set('assets', resolve('src/assets'))
      .set('components', resolve('src/components'))
      .set('views', resolve('src/views'))

    config.optimization.minimizer('terser').tap((args) => {
      // 去除生产环境console
      args[0].terserOptions.compress.drop_console = true
      return args
    })
  },

  // 是否为 Babel 或 TypeScript 使用 thread-loader。该选项在系统的 CPU 有多于一个内核时自动启用,仅作用于生产构建。
  parallel: require('os').cpus().length > 1,

  devServer: {
    host: '0.0.0.0',
    port: 8088, // 端口号
    https: false, // https:{type:Boolean}
    open: false, // 配置自动启动浏览器  open: 'Google Chrome'-默认启动谷歌

    // 配置多个代理
    proxy: {
      '/api': {
        target: 'https://www.mock.com',
        ws: true, // 代理的WebSockets
        changeOrigin: true, // 允许websockets跨域
        pathRewrite: {
          '^/api': '',
        },
      },
    },
  },
}

基础组件封装

在开发项目过程中,通常会用到很多功能和设计相类似的组件,toast 和 dialog 组件基本是每一个移动端项目都会用到的。为了更好匹配自己公司的 UI 设计风格,我们没有直接用 vant 的 toast 和 dialog 组件,而是自己封装了类似的组件,可供直接调用,如:

this.$toast({ msg: '手机号码不能为空' })

this.$toast({
  msg: '成功提示',
  type: 'success',
})

this.$dialog({
  title: '删除提示',
  text: '是否确定删除此标签?',
  showCancelBtn: true,
  confirmText: '确认',
  confirm(content) {
    alert('删除成功')
  },
})

效果图如下

webpack 可视化分析

从这里开始,我们开始进行 webpack 优化打包。首先我们来分析一下 webpack 打包性能瓶颈,找出问题所在,然后才能对症下药。此时就用到 webpack-bundle-analyzer 了。 1、安装依赖

npm install webpack-bundle-analyzer -D

2、在 vue.config.js 配置

const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
configureWebpack: (config) => {
  if (process.env.NODE_ENV === 'production') {
    config.plugins.push(new BundleAnalyzerPlugin())
  }
}

打包后,我们可以看到这样一份依赖图

从以上的界面中,我们可以得到以下信息:

  • 打包出的文件中都包含了什么,以及模块之间的依赖关系
  • 每个文件的大小在总体中的占比,找出较大的文件,思考是否有替换方案,是否使用了它包含了不必要的依赖?
  • 是否有重复的依赖项,对此可以如何优化?
  • 每个文件的压缩后的大小。

CDN 资源优化

CDN 的全称是 Content Delivery Network,即内容分发网络。CDN 是构建在网络之上的内容分发网络,依靠部署在各地的边缘服务器,通过中心平台的负载均衡、内容分发、调度等功能模块,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。CDN 的关键技术主要有内容存储和分发技术。

随着项目越做越大,依赖的第三方 npm 包越来越多,构建之后的文件也会越来越大。再加上又是单页应用,这就会导致在网速较慢或者服务器带宽有限的情况出现长时间的白屏。此时我们可以使用 CDN 的方法,优化网络加载速度。

1、将 vue、vue-router、vuex、axios 这些 vue 全家桶的资源,全部改为通过 CDN 链接获取,在 index.html 里插入 相应链接。

<body>
  <div id="app"></div>
  <script data-original="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
  <script data-original="https://cdn.bootcss.com/axios/0.19.0-beta.1/axios.min.js"></script>
  <script data-original="https://cdn.bootcss.com/vuex/3.1.0/vuex.min.js"></script>
  <script data-original="https://cdn.bootcss.com/vue-router/3.0.2/vue-router.min.js"></script>
  <script data-original="https://cdn.bootcss.com/element-ui/2.6.1/index.js"></script>
</body>

2、在 vue.config.js 配置 externals 属性

module.exports = {
 ···
    externals: {
      'vue': 'Vue',
      'vuex': 'Vuex',
      'vue-router': 'VueRouter',
      'axios':'axios'
    }
  }

3、卸载相关依赖的 npm 包

npm uninstall  vue vue-router vuex axios

此时启动项目运行就可以了。我们在控制台就能发现项目加载了以上四个 CDN 资源。

不过现在有不少声音说,vue 全家桶加载 CDN 资源其实作用并不大,而且公共的 CDN 资源也没有 npm 包那么稳定,这个就见仁见智了。所以我在源码时新建的分支做这个优化。当项目较小的就不考虑 CDN 优化了。

当然,当引入其他较大第三方资源,比如 echarts,AMAP(高德地图),采用 CDN 资源还是很有必要的。

gZip 加速优化

所有现代浏览器都支持 gzip 压缩,启用 gzip 压缩可大幅缩减传输资源大小,从而缩短资源下载时间,减少首次白屏时间,提升用户体验。

gzip 对基于文本格式文件的压缩效果最好(如:CSS、JavaScript 和 HTML),在压缩较大文件时往往可实现高达 70-90% 的压缩率,对已经压缩过的资源(如:图片)进行 gzip 压缩处理,效果很不好。

const CompressionPlugin = require('compression-webpack-plugin')
configureWebpack: (config) => {
  if (process.env.NODE_ENV === 'production') {
    config.plugins.push(
      new CompressionPlugin({
        // gzip压缩配置
        test: /\.js$|\.html$|\.css/, // 匹配文件名
        threshold: 10240, // 对超过10kb的数据进行压缩
        deleteOriginalAssets: false, // 是否删除原文件
      })
    )
  }
}

首页添加骨架屏

随着 SPA 在前端界的逐渐流行,单页面应用不可避免地给首页加载带来压力,此时良好的首页用户体验至关重要。很多 APP 采用了“骨架屏”的方式去展示未加载内容,给予了用户焕然一新的体验。

所谓的骨架屏,就是在页面内容未加载完成的时候,先使用一些图形进行占位,待内容加载完成之后再把它替换掉。在这个过程中用户会感知到内容正在逐渐加载并即将呈现,降低了“白屏”的不良体验。

本文采用vue-skeleton-webpack-plugin插件为单页面应用注入骨架屏。

1、在src的common文件夹下面创建了Skeleton1.vue,Skeleton2.vue,具体的结构和样式自行设计,此处省略一万字。。。。

2、在同级目录下新建entry-skeleton.js

import Vue from 'vue'
import Skeleton1 from './Skeleton1'
import Skeleton2 from './Skeleton2'

export default new Vue({
  components: {
    Skeleton1,
    Skeleton2
  },
  template: `
    <div>
      <skeleton1 id="skeleton1" />
      <skeleton2 id="skeleton2" />
    </div>
  `
})

在vue.config.js下配置插件

const SkeletonWebpackPlugin = require('vue-skeleton-webpack-plugin')
configureWebpack: (config) => {
  config.plugins.push(
    new SkeletonWebpackPlugin({
      webpackConfig: {
        entry: {
          app: path.join(__dirname, './src/common/entry-skeleton.js'),
        },
      },
      minimize: true,
      quiet: true,
      router: {
        mode: 'hash',
        routes: [
          { path: '/', skeletonId: 'skeleton1' },
          { path: '/about', skeletonId: 'skeleton2' },
        ],
      },
    })
  )
}

此时重新加载页面就可以看到我们的骨架屏了。注意:一定要配置样式分离extract: true

推荐文章

搭建一个vue-cli的移动端H5开发模板
封装一个toast和dialog组件并发布到npm
从零开始构建一个webpack项目
总结几个webpack打包优化的方法
总结vue知识体系之高级应用篇
总结vue知识体系之实用技巧
总结vue知识体系之基础入门篇
总结移动端H5开发常用技巧(干货满满哦!)

查看原文

须臾 回答了问题 · 2019-10-25

解决前端input调起的系统键盘如何不监听上下键?

可以考虑每一个input都做一个仿造的,当输入的时候只显示正在输入的这个input,其他input都隐藏,这样切换tab是不会切换到下一个input的,我之前有一个项目就是这么做的。

关注 5 回答 4

须臾 关注了标签 · 2019-10-25

前端

Web前端开发是从网页制作演变而来的,名称上有很明显的时代特征。在互联网的演化进程中,网页制作是Web 1.0时代的产物,那时网站的主要内容都是静态的,用户使用网站的行为也以浏览为主。2005年以后,互联网进入Web 2.0时代,各种类似桌面软件的Web应用大量涌现,网站的前端由此发生了翻天覆地的变化。网页不再只是承载单一的文字和图片,各种富媒体让网页的内容更加生动,网页上软件化的交互形式为用户提供了更好的使用体验,这些都是基于前端技术实现的。

Web前端优化
  1. 尽量减少HTTP请求 (Make Fewer HTTP Requests)
  2. 减少 DNS 查找 (Reduce DNS Lookups)
  3. 避免重定向 (Avoid Redirects)
  4. 使得 Ajax 可缓存 (Make Ajax Cacheable)
  5. 延迟载入组件 (Post-load Components)
  6. 预载入组件 (Preload Components)
  7. 减少 DOM 元素数量 (Reduce the Number of DOM Elements)
  8. 切分组件到多个域 (Split Components Across Domains)
  9. 最小化 iframe 的数量 (Minimize the Number of iframes)
  10. 杜绝 http 404 错误 (No 404s)

关注 189605

须臾 回答了问题 · 2019-10-17

vue请求本地后台接口提示跨域怎么办?

请求的端口也需要一致

关注 3 回答 3

须臾 回答了问题 · 2019-10-15

解决【淘宝留言输入框】发现一个难题,通过JS代码无法修改其中内容

我试了你的代码可以修改啊

关注 3 回答 3

须臾 回答了问题 · 2019-09-27

解决克隆别人仓库的代码到自己的本地版本库,只能克隆master分支的代码吗

git clone -b <指定分支名> <远程仓库地址>

关注 5 回答 4

须臾 赞了回答 · 2019-09-20

解决Vue点击切换样式

clipboard.png
.fill()对象的话,所有对象都是指向同一个地址的。改一个动全部。

所以@click没问题,是测试用例没写好。

关注 5 回答 4

认证与成就

  • 获得 19 次点赞
  • 获得 76 枚徽章 获得 4 枚金徽章, 获得 22 枚银徽章, 获得 50 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-04-19
个人主页被 883 人浏览