达摩师兄

达摩师兄 查看完整档案

西安编辑武汉理工大学  |  软件工程 编辑  |  填写所在公司/组织 blog.usword.cn 编辑
编辑

欲取鸣琴弹,恨无知音赏

个人动态

达摩师兄 赞了回答 · 4月13日

解决前端如何百分百还原美工图?

如果允许页面出现滚动条,实现还是有可能的,宽高按设计稿的比例计算出来,

如果,既要与设计稿一致,又要整屏显示,那就是不可能的,除非做设计的,也根据不同的设备设计出不同大小的设计稿来,

说多了都是泪,如果你的上司是做设计的,拿着一张设计稿要前端出一模一样的页面,还要整屏显示,因为设计稿就是没有滚动条的,这时你就直接拿键盘鼠标砸到他脸上

关注 17 回答 12

达摩师兄 回答了问题 · 4月9日

在setInterval中使用await没有效果

你没有搞懂even loop 建议搞懂这个 这个基本操作

关注 6 回答 4

达摩师兄 赞了文章 · 3月13日

webpack原理

webpack原理

查看所有文档页面:前端开发文档,获取更多信息。
原文链接:webpack原理,原文广告模态框遮挡,阅读体验不好,所以整理成本文,方便查找。

工作原理概括

基本概念

在了解 Webpack 原理前,需要掌握以下几个核心概念,以方便后面的理解:

  • Entry:入口,Webpack 执行构建的第一步将从 Entry 开始,可抽象成输入。
  • Module:模块,在 Webpack 里一切皆模块,一个模块对应着一个文件。Webpack 会从配置的 Entry 开始递归找出所有依赖的模块。
  • Chunk:代码块,一个 Chunk 由多个模块组合而成,用于代码合并与分割。
  • Loader:模块转换器,用于把模块原内容按照需求转换成新内容。
  • Plugin:扩展插件,在 Webpack 构建流程中的特定时机会广播出对应的事件,插件可以监听这些事件的发生,在特定时机做对应的事情。

流程概括

Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:

  1. 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数;
  2. 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译;
  3. 确定入口:根据配置中的 entry 找出所有的入口文件;
  4. 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;
  5. 完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系;
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;
  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。

在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。

流程细节

Webpack 的构建流程可以分为以下三大阶段:

  1. 初始化:启动构建,读取与合并配置参数,加载 Plugin,实例化 Compiler。
  2. 编译:从 Entry 发出,针对每个 Module 串行调用对应的 Loader 去翻译文件内容,再找到该 Module 依赖的 Module,递归地进行编译处理。
  3. 输出:对编译后的 Module 组合成 Chunk,把 Chunk 转换成文件,输出到文件系统。

如果只执行一次构建,以上阶段将会按照顺序各执行一次。但在开启监听模式下,流程将变为如下:

在每个大阶段中又会发生很多事件,Webpack 会把这些事件广播出来供给 Plugin 使用,下面来一一介绍。

初始化阶段

事件名解释
初始化参数从配置文件和 Shell 语句中读取与合并参数,得出最终的参数。 这个过程中还会执行配置文件中的插件实例化语句 new Plugin()
实例化 Compiler用上一步得到的参数初始化 Compiler 实例,Compiler 负责文件监听和启动编译。Compiler 实例中包含了完整的 Webpack 配置,全局只有一个 Compiler 实例。
加载插件依次调用插件的 apply 方法,让插件可以监听后续的所有事件节点。同时给插件传入 compiler 实例的引用,以方便插件通过 compiler 调用 Webpack 提供的 API。
environment开始应用 Node.js 风格的文件系统到 compiler 对象,以方便后续的文件寻找和读取。
entry-option读取配置的 Entrys,为每个 Entry 实例化一个对应的 EntryPlugin,为后面该 Entry 的递归解析工作做准备。
after-plugins调用完所有内置的和配置的插件的 apply 方法。
after-resolvers根据配置初始化完 resolverresolver 负责在文件系统中寻找指定路径的文件。
空格空格
空格空格
空格空格

编译阶段

事件名解释
run启动一次新的编译。
watch-runrun 类似,区别在于它是在监听模式下启动的编译,在这个事件中可以获取到是哪些文件发生了变化导致重新启动一次新的编译。
compile该事件是为了告诉插件一次新的编译将要启动,同时会给插件带上 compiler 对象。
compilationWebpack 以开发模式运行时,每当检测到文件变化,一次新的 Compilation 将被创建。一个 Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。Compilation 对象也提供了很多事件回调供插件做扩展。
make一个新的 Compilation 创建完毕,即将从 Entry 开始读取文件,根据文件类型和配置的 Loader 对文件进行编译,编译完后再找出该文件依赖的文件,递归的编译和解析。
after-compile一次 Compilation 执行完成。
invalid当遇到文件不存在、文件编译错误等异常时会触发该事件,该事件不会导致 Webpack 退出。
空格空格
空格空格

在编译阶段中,最重要的要数 compilation 事件了,因为在 compilation 阶段调用了 Loader 完成了每个模块的转换操作,在 compilation 阶段又包括很多小的事件,它们分别是:

事件名解释
build-module使用对应的 Loader 去转换一个模块。
normal-module-loader在用 Loader 对一个模块转换完后,使用 acorn 解析转换后的内容,输出对应的抽象语法树(AST),以方便 Webpack 后面对代码的分析。
program从配置的入口模块开始,分析其 AST,当遇到 require 等导入其它模块语句时,便将其加入到依赖的模块列表,同时对新找出的依赖模块递归分析,最终搞清所有模块的依赖关系。
seal所有模块及其依赖的模块都通过 Loader 转换完成后,根据依赖关系开始生成 Chunk。

输出阶段

事件名解释
should-emit所有需要输出的文件已经生成好,询问插件哪些文件需要输出,哪些不需要。
emit确定好要输出哪些文件后,执行文件输出,可以在这里获取和修改输出内容。
after-emit文件输出完毕。
done成功完成一次完成的编译和输出流程。
failed如果在编译和输出流程中遇到异常导致 Webpack 退出时,就会直接跳转到本步骤,插件可以在本事件中获取到具体的错误原因。

在输出阶段已经得到了各个模块经过转换后的结果和其依赖关系,并且把相关模块组合在一起形成一个个 Chunk。 在输出阶段会根据 Chunk 的类型,使用对应的模版生成最终要要输出的文件内容。

输出文件分析

虽然在前面的章节中你学会了如何使用 Webpack ,也大致知道其工作原理,可是你想过 Webpack 输出的 bundle.js 是什么样子的吗? 为什么原来一个个的模块文件被合并成了一个单独的文件?为什么 bundle.js 能直接运行在浏览器中? 本节将解释清楚以上问题。

先来看看由 安装与使用 中最简单的项目构建出的 bundle.js 文件内容,代码如下:

<p data-height="565" data-theme-id="0" data-slug-hash="NMQzxz" data-default-tab="js" data-user="whjin" data-embed-version="2" data-pen-title="bundle.js" class="codepen">See the Pen bundle.js by whjin (@whjin) on CodePen.</p>
<script async data-original="https://static.codepen.io/ass...;></script>

以上看上去复杂的代码其实是一个立即执行函数,可以简写为如下:

(function(modules) {

  // 模拟 require 语句
  function __webpack_require__() {
  }

  // 执行存放所有模块数组中的第0个模块
  __webpack_require__(0);

})([/*存放所有模块的数组*/])

bundle.js 能直接运行在浏览器中的原因在于输出的文件中通过 __webpack_require__ 函数定义了一个可以在浏览器中执行的加载函数来模拟 Node.js 中的 require 语句。

原来一个个独立的模块文件被合并到了一个单独的 bundle.js 的原因在于浏览器不能像 Node.js 那样快速地去本地加载一个个模块文件,而必须通过网络请求去加载还未得到的文件。 如果模块数量很多,加载时间会很长,因此把所有模块都存放在了数组中,执行一次网络加载。

如果仔细分析 __webpack_require__ 函数的实现,你还有发现 Webpack 做了缓存优化: 执行加载过的模块不会再执行第二次,执行结果会缓存在内存中,当某个模块第二次被访问时会直接去内存中读取被缓存的返回值。

分割代码时的输出

例如把源码中的 main.js 修改为如下:

// 异步加载 show.js
import('./show').then((show) => {
  // 执行 show 函数
  show('Webpack');
});

重新构建后会输出两个文件,分别是执行入口文件 bundle.js 和 异步加载文件 0.bundle.js

其中 0.bundle.js 内容如下:

// 加载在本文件(0.bundle.js)中包含的模块
webpackJsonp(
  // 在其它文件中存放着的模块的 ID
  [0],
  // 本文件所包含的模块
  [
    // show.js 所对应的模块
    (function (module, exports) {
      function show(content) {
        window.document.getElementById('app').innerText = 'Hello,' + content;
      }

      module.exports = show;
    })
  ]
);

bundle.js 内容如下:

<p data-height="565" data-theme-id="0" data-slug-hash="yjmRyG" data-default-tab="js" data-user="whjin" data-embed-version="2" data-pen-title="bundle.js" class="codepen">See the Pen bundle.js by whjin (@whjin) on CodePen.</p>
<script async data-original="https://static.codepen.io/ass...;></script>

这里的 bundle.js 和上面所讲的 bundle.js 非常相似,区别在于:

  • 多了一个 __webpack_require__.e 用于加载被分割出去的,需要异步加载的 Chunk 对应的文件;
  • 多了一个 webpackJsonp 函数用于从异步加载的文件中安装模块。

在使用了 CommonsChunkPlugin 去提取公共代码时输出的文件和使用了异步加载时输出的文件是一样的,都会有 __webpack_require__.ewebpackJsonp。 原因在于提取公共代码和异步加载本质上都是代码分割。

编写 Loader

Loader 就像是一个翻译员,能把源文件经过转化后输出新的结果,并且一个文件还可以链式的经过多个翻译员翻译。

以处理 SCSS 文件为例:

  • SCSS 源代码会先交给 sass-loader 把 SCSS 转换成 CSS;
  • sass-loader 输出的 CSS 交给 css-loader 处理,找出 CSS 中依赖的资源、压缩 CSS 等;
  • css-loader 输出的 CSS 交给 style-loader 处理,转换成通过脚本加载的 JavaScript 代码;

可以看出以上的处理过程需要有顺序的链式执行,先 sass-loadercss-loaderstyle-loader。 以上处理的 Webpack 相关配置如下:

<p data-height="365" data-theme-id="0" data-slug-hash="YLmbeQ" data-default-tab="js" data-user="whjin" data-embed-version="2" data-pen-title="编写 Loader" class="codepen">See the Pen 编写 Loader by whjin (@whjin) on CodePen.</p>
<script async data-original="https://static.codepen.io/ass...;></script>

Loader 的职责

由上面的例子可以看出:一个 Loader 的职责是单一的,只需要完成一种转换。 如果一个源文件需要经历多步转换才能正常使用,就通过多个 Loader 去转换。 在调用多个 Loader 去转换一个文件时,每个 Loader 会链式的顺序执行, 第一个 Loader 将会拿到需处理的原内容,上一个 Loader 处理后的结果会传给下一个接着处理,最后的 Loader 将处理后的最终结果返回给 Webpack。

所以,在你开发一个 Loader 时,请保持其职责的单一性,你只需关心输入和输出。

Loader 基础

由于 Webpack 是运行在 Node.js 之上的,一个 Loader 其实就是一个 Node.js 模块,这个模块需要导出一个函数。 这个导出的函数的工作就是获得处理前的原内容,对原内容执行处理后,返回处理后的内容。

一个最简单的 Loader 的源码如下:

module.exports = function(source) {
  // source 为 compiler 传递给 Loader 的一个文件的原内容
  // 该函数需要返回处理后的内容,这里简单起见,直接把原内容返回了,相当于该 Loader 没有做任何转换
  return source;
};

由于 Loader 运行在 Node.js 中,你可以调用任何 Node.js 自带的 API,或者安装第三方模块进行调用:

const sass = require('node-sass');
module.exports = function(source) {
  return sass(source);
};

Loader 进阶

以上只是个最简单的 Loader,Webpack 还提供一些 API 供 Loader 调用,下面来一一介绍。

获得 Loader 的 options

在最上面处理 SCSS 文件的 Webpack 配置中,给 css-loader 传了 options 参数,以控制 css-loader。 如何在自己编写的 Loader 中获取到用户传入的 options 呢?需要这样做:

const loaderUtils = require('loader-utils');
module.exports = function(source) {
  // 获取到用户给当前 Loader 传入的 options
  const options = loaderUtils.getOptions(this);
  return source;
};

返回其它结果

上面的 Loader 都只是返回了原内容转换后的内容,但有些场景下还需要返回除了内容之外的东西。

例如以用 babel-loader 转换 ES6 代码为例,它还需要输出转换后的 ES5 代码对应的 Source Map,以方便调试源码。 为了把 Source Map 也一起随着 ES5 代码返回给 Webpack,可以这样写:

module.exports = function(source) {
  // 通过 this.callback 告诉 Webpack 返回的结果
  this.callback(null, source, sourceMaps);
  // 当你使用 this.callback 返回内容时,该 Loader 必须返回 undefined,
  // 以让 Webpack 知道该 Loader 返回的结果在 this.callback 中,而不是 return 中 
  return;
};

其中的 this.callback 是 Webpack 给 Loader 注入的 API,以方便 Loader 和 Webpack 之间通信。 this.callback 的详细使用方法如下:

this.callback(
    // 当无法转换原内容时,给 Webpack 返回一个 Error
    err: Error | null,
    // 原内容转换后的内容
    content: string | Buffer,
    // 用于把转换后的内容得出原内容的 Source Map,方便调试
    sourceMap?: SourceMap,
    // 如果本次转换为原内容生成了 AST 语法树,可以把这个 AST 返回,
    // 以方便之后需要 AST 的 Loader 复用该 AST,以避免重复生成 AST,提升性能
    abstractSyntaxTree?: AST
);
Source Map 的生成很耗时,通常在开发环境下才会生成 Source Map,其它环境下不用生成,以加速构建。 为此 Webpack 为 Loader 提供了 this.sourceMap API 去告诉 Loader 当前构建环境下用户是否需要 Source Map。 如果你编写的 Loader 会生成 Source Map,请考虑到这点。

同步与异步

Loader 有同步和异步之分,上面介绍的 Loader 都是同步的 Loader,因为它们的转换流程都是同步的,转换完成后再返回结果。 但在有些场景下转换的步骤只能是异步完成的,例如你需要通过网络请求才能得出结果,如果采用同步的方式网络请求就会阻塞整个构建,导致构建非常缓慢。

在转换步骤是异步时,你可以这样:

module.exports = function(source) {
    // 告诉 Webpack 本次转换是异步的,Loader 会在 callback 中回调结果
    var callback = this.async();
    someAsyncOperation(source, function(err, result, sourceMaps, ast) {
        // 通过 callback 返回异步执行后的结果
        callback(err, result, sourceMaps, ast);
    });
};

处理二进制数据

在默认的情况下,Webpack 传给 Loader 的原内容都是 UTF-8 格式编码的字符串。 但有些场景下 Loader 不是处理文本文件,而是处理二进制文件,例如 file-loader,就需要 Webpack 给 Loader 传入二进制格式的数据。 为此,你需要这样编写 Loader:

module.exports = function(source) {
    // 在 exports.raw === true 时,Webpack 传给 Loader 的 source 是 Buffer 类型的
    source instanceof Buffer === true;
    // Loader 返回的类型也可以是 Buffer 类型的
    // 在 exports.raw !== true 时,Loader 也可以返回 Buffer 类型的结果
    return source;
};
// 通过 exports.raw 属性告诉 Webpack 该 Loader 是否需要二进制数据 
module.exports.raw = true;

以上代码中最关键的代码是最后一行 module.exports.raw = true;,没有该行 Loader 只能拿到字符串。

缓存加速

在有些情况下,有些转换操作需要大量计算非常耗时,如果每次构建都重新执行重复的转换操作,构建将会变得非常缓慢。 为此,Webpack 会默认缓存所有 Loader 的处理结果,也就是说在需要被处理的文件或者其依赖的文件没有发生变化时, 是不会重新调用对应的 Loader 去执行转换操作的。

如果你想让 Webpack 不缓存该 Loader 的处理结果,可以这样:

module.exports = function(source) {
  // 关闭该 Loader 的缓存功能
  this.cacheable(false);
  return source;
};

其它 Loader API

除了以上提到的在 Loader 中能调用的 Webpack API 外,还存在以下常用 API:

  • this.context:当前处理文件的所在目录,假如当前 Loader 处理的文件是 /src/main.js,则 this.context 就等于 /src
  • this.resource:当前处理文件的完整请求路径,包括 querystring,例如 /src/main.js?name=1
  • this.resourcePath:当前处理文件的路径,例如 /src/main.js
  • this.resourceQuery:当前处理文件的 querystring
  • this.target:等于 Webpack 配置中的 Target。
  • this.loadModule:但 Loader 在处理一个文件时,如果依赖其它文件的处理结果才能得出当前文件的结果时, 就可以通过 this.loadModule(request: string, callback: function(err, source, sourceMap, module)) 去获得 request 对应文件的处理结果。
  • this.resolve:像 require 语句一样获得指定文件的完整路径,使用方法为 resolve(context: string, request: string, callback: function(err, result: string))
  • this.addDependency:给当前处理文件添加其依赖的文件,以便再其依赖的文件发生变化时,会重新调用 Loader 处理该文件。使用方法为 addDependency(file: string)
  • this.addContextDependency:和 addDependency 类似,但 addContextDependency 是把整个目录加入到当前正在处理文件的依赖中。使用方法为 addContextDependency(directory: string)
  • this.clearDependencies:清除当前正在处理文件的所有依赖,使用方法为 clearDependencies()
  • this.emitFile:输出一个文件,使用方法为 emitFile(name: string, content: Buffer|string, sourceMap: {...})

加载本地 Loader

在开发 Loader 的过程中,为了测试编写的 Loader 是否能正常工作,需要把它配置到 Webpack 中后,才可能会调用该 Loader。 在前面的章节中,使用的 Loader 都是通过 Npm 安装的,要使用 Loader 时会直接使用 Loader 的名称,代码如下:

module.exports = {
  module: {
    rules: [
      {
        test: /\.css/,
        use: ['style-loader'],
      },
    ]
  },
};

如果还采取以上的方法去使用本地开发的 Loader 将会很麻烦,因为你需要确保编写的 Loader 的源码是在 node_modules 目录下。 为此你需要先把编写的 Loader 发布到 Npm 仓库后再安装到本地项目使用。

解决以上问题的便捷方法有两种,分别如下:

Npm link

Npm link 专门用于开发和调试本地 Npm 模块,能做到在不发布模块的情况下,把本地的一个正在开发的模块的源码链接到项目的 node_modules 目录下,让项目可以直接使用本地的 Npm 模块。 由于是通过软链接的方式实现的,编辑了本地的 Npm 模块代码,在项目中也能使用到编辑后的代码。

完成 Npm link 的步骤如下:

  • 确保正在开发的本地 Npm 模块(也就是正在开发的 Loader)的 package.json 已经正确配置好;
  • 在本地 Npm 模块根目录下执行 npm link,把本地模块注册到全局;
  • 在项目根目录下执行 npm link loader-name,把第2步注册到全局的本地 Npm 模块链接到项目的 node_moduels 下,其中的 loader-name 是指在第1步中的 package.json 文件中配置的模块名称。

链接好 Loader 到项目后你就可以像使用一个真正的 Npm 模块一样使用本地的 Loader 了。

ResolveLoader

ResolveLoader 用于配置 Webpack 如何寻找 Loader。 默认情况下只会去 node_modules 目录下寻找,为了让 Webpack 加载放在本地项目中的 Loader 需要修改 resolveLoader.modules

假如本地的 Loader 在项目目录中的 ./loaders/loader-name 中,则需要如下配置:


module.exports = {
  resolveLoader:{
    // 去哪些目录下寻找 Loader,有先后顺序之分
    modules: ['node_modules','./loaders/'],
  }
}

加上以上配置后, Webpack 会先去 node_modules 项目下寻找 Loader,如果找不到,会再去 ./loaders/ 目录下寻找。

实战

上面讲了许多理论,接下来从实际出发,来编写一个解决实际问题的 Loader。

该 Loader 名叫 comment-require-loader,作用是把 JavaScript 代码中的注释语法:

// @require '../style/index.css'

转换成:

require('../style/index.css');

该 Loader 的使用场景是去正确加载针对 Fis3 编写的 JavaScript,这些 JavaScript 中存在通过注释的方式加载依赖的 CSS 文件。

该 Loader 的使用方法如下:

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: ['comment-require-loader'],
        // 针对采用了 fis3 CSS 导入语法的 JavaScript 文件通过 comment-require-loader 去转换 
        include: [path.resolve(__dirname, 'node_modules/imui')]
      }
    ]
  }
};

该 Loader 的实现非常简单,完整代码如下:

function replace(source) {
    // 使用正则把 // @require '../style/index.css' 转换成 require('../style/index.css');  
    return source.replace(/(\/\/ *@require) +(('|").+('|")).*/, 'require($2);');
}

module.exports = function (content) {
    return replace(content);
};

编写 Plugin

Webpack 通过 Plugin 机制让其更加灵活,以适应各种应用场景。 在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。

一个最基础的 Plugin 的代码是这样的:

class BasicPlugin{
  // 在构造函数中获取用户给该插件传入的配置
  constructor(options){
  }

  // Webpack 会调用 BasicPlugin 实例的 apply 方法给插件实例传入 compiler 对象
  apply(compiler){
    compiler.plugin('compilation',function(compilation) {
    })
  }
}

// 导出 Plugin
module.exports = BasicPlugin;

在使用这个 Plugin 时,相关配置代码如下:

const BasicPlugin = require('./BasicPlugin.js');
module.export = {
  plugins:[
    new BasicPlugin(options),
  ]
}

Webpack 启动后,在读取配置的过程中会先执行 new BasicPlugin(options) 初始化一个 BasicPlugin 获得其实例。 在初始化 compiler 对象后,再调用 basicPlugin.apply(compiler) 给插件实例传入 compiler 对象。 插件实例在获取到 compiler 对象后,就可以通过 compiler.plugin(事件名称, 回调函数) 监听到 Webpack 广播出来的事件。 并且可以通过 compiler 对象去操作 Webpack。

通过以上最简单的 Plugin 相信你大概明白了 Plugin 的工作原理,但实际开发中还有很多细节需要注意,下面来详细介绍。

CompilerCompilation

在开发 Plugin 时最常用的两个对象就是 Compiler 和 Compilation,它们是 Plugin 和 Webpack 之间的桥梁。 Compiler 和 Compilation 的含义如下:

  • Compiler 对象包含了 Webpack 环境所有的的配置信息,包含 optionsloadersplugins 这些信息,这个对象在 Webpack 启动时候被实例化,它是全局唯一的,可以简单地把它理解为 Webpack 实例;
  • Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。当 Webpack 以开发模式运行时,每当检测到一个文件变化,一次新的 Compilation 将被创建。Compilation 对象也提供了很多事件回调供插件做扩展。通过 Compilation 也能读取到 Compiler 对象。

Compiler 和 Compilation 的区别在于:Compiler 代表了整个 Webpack 从启动到关闭的生命周期,而 Compilation 只是代表了一次新的编译。

事件流

Webpack 就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果。 这条生产线上的每个处理流程的职责都是单一的,多个流程之间有存在依赖关系,只有完成当前处理后才能交给下一个流程去处理。 插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源做处理。

Webpack 通过 Tapable 来组织这条复杂的生产线。 Webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运作。 Webpack 的事件流机制保证了插件的有序性,使得整个系统扩展性很好。

Webpack 的事件流机制应用了观察者模式,和 Node.js 中的 EventEmitter 非常相似。Compiler 和 Compilation 都继承自 Tapable,可以直接在 Compiler 和 Compilation 对象上广播和监听事件,方法如下:

/**
* 广播出事件
* event-name 为事件名称,注意不要和现有的事件重名
* params 为附带的参数
*/
compiler.apply('event-name',params);

/**
* 监听名称为 event-name 的事件,当 event-name 事件发生时,函数就会被执行。
* 同时函数中的 params 参数为广播事件时附带的参数。
*/
compiler.plugin('event-name',function(params) {

});

同理,compilation.applycompilation.plugin 使用方法和上面一致。

在开发插件时,你可能会不知道该如何下手,因为你不知道该监听哪个事件才能完成任务。

在开发插件时,还需要注意以下两点:

  • 只要能拿到 Compiler 或 Compilation 对象,就能广播出新的事件,所以在新开发的插件中也能广播出事件,给其它插件监听使用。
  • 传给每个插件的 Compiler 和 Compilation 对象都是同一个引用。也就是说在一个插件中修改了 Compiler 或 Compilation 对象上的属性,会影响到后面的插件。
  • 有些事件是异步的,这些异步的事件会附带两个参数,第二个参数为回调函数,在插件处理完任务时需要调用回调函数通知 Webpack,才会进入下一处理流程。例如:
 compiler.plugin('emit',function(compilation, callback) {
    // 支持处理逻辑

    // 处理完毕后执行 callback 以通知 Webpack 
    // 如果不执行 callback,运行流程将会一直卡在这不往下执行 
    callback();
  });

常用 API

插件可以用来修改输出文件、增加输出文件、甚至可以提升 Webpack 性能、等等,总之插件通过调用 Webpack 提供的 API 能完成很多事情。 由于 Webpack 提供的 API 非常多,有很多 API 很少用的上,又加上篇幅有限,下面来介绍一些常用的 API。

读取输出资源、代码块、模块及其依赖

有些插件可能需要读取 Webpack 的处理结果,例如输出资源、代码块、模块及其依赖,以便做下一步处理。

emit 事件发生时,代表源文件的转换和组装已经完成,在这里可以读取到最终将输出的资源、代码块、模块及其依赖,并且可以修改输出资源的内容。 插件代码如下:

<p data-height="585" data-theme-id="0" data-slug-hash="RJwjPj" data-default-tab="js" data-user="whjin" data-embed-version="2" data-pen-title="emit" class="codepen">See the Pen emit by whjin (@whjin) on CodePen.</p>
<script async data-original="https://static.codepen.io/ass...;></script>

监听文件变化

Webpack 会从配置的入口模块出发,依次找出所有的依赖模块,当入口模块或者其依赖的模块发生变化时, 就会触发一次新的 Compilation。

在开发插件时经常需要知道是哪个文件发生变化导致了新的 Compilation,为此可以使用如下代码:

<p data-height="255" data-theme-id="0" data-slug-hash="jKOabJ" data-default-tab="js" data-user="whjin" data-embed-version="2" data-pen-title="Compilation" class="codepen">See the Pen Compilation by whjin (@whjin) on CodePen.</p>
<script async data-original="https://static.codepen.io/ass...;></script>

默认情况下 Webpack 只会监视入口和其依赖的模块是否发生变化,在有些情况下项目可能需要引入新的文件,例如引入一个 HTML 文件。 由于 JavaScript 文件不会去导入 HTML 文件,Webpack 就不会监听 HTML 文件的变化,编辑 HTML 文件时就不会重新触发新的 Compilation。 为了监听 HTML 文件的变化,我们需要把 HTML 文件加入到依赖列表中,为此可以使用如下代码:

compiler.plugin('after-compile', (compilation, callback) => {
  // 把 HTML 文件添加到文件依赖列表,好让 Webpack 去监听 HTML 模块文件,在 HTML 模版文件发生变化时重新启动一次编译
    compilation.fileDependencies.push(filePath);
    callback();
});

修改输出资源

有些场景下插件需要修改、增加、删除输出的资源,要做到这点需要监听 emit 事件,因为发生 emit 事件时所有模块的转换和代码块对应的文件已经生成好, 需要输出的资源即将输出,因此 emit 事件是修改 Webpack 输出资源的最后时机。

所有需要输出的资源会存放在 compilation.assets 中,compilation.assets 是一个键值对,键为需要输出的文件名称,值为文件对应的内容。

设置 compilation.assets 的代码如下:

compiler.plugin('emit', (compilation, callback) => {
  // 设置名称为 fileName 的输出资源
  compilation.assets[fileName] = {
    // 返回文件内容
    source: () => {
      // fileContent 既可以是代表文本文件的字符串,也可以是代表二进制文件的 Buffer
      return fileContent;
      },
    // 返回文件大小
      size: () => {
      return Buffer.byteLength(fileContent, 'utf8');
    }
  };
  callback();
});

读取 compilation.assets 的代码如下:


compiler.plugin('emit', (compilation, callback) => {
  // 读取名称为 fileName 的输出资源
  const asset = compilation.assets[fileName];
  // 获取输出资源的内容
  asset.source();
  // 获取输出资源的文件大小
  asset.size();
  callback();
});

判断 Webpack 使用了哪些插件

在开发一个插件时可能需要根据当前配置是否使用了其它某个插件而做下一步决定,因此需要读取 Webpack 当前的插件配置情况。 以判断当前是否使用了 ExtractTextPlugin 为例,可以使用如下代码:

// 判断当前配置使用使用了 ExtractTextPlugin,
// compiler 参数即为 Webpack 在 apply(compiler) 中传入的参数
function hasExtractTextPlugin(compiler) {
  // 当前配置所有使用的插件列表
  const plugins = compiler.options.plugins;
  // 去 plugins 中寻找有没有 ExtractTextPlugin 的实例
  return plugins.find(plugin=>plugin.__proto__.constructor === ExtractTextPlugin) != null;
}

实战

下面我们举一个实际的例子,带你一步步去实现一个插件。

该插件的名称取名叫 EndWebpackPlugin,作用是在 Webpack 即将退出时再附加一些额外的操作,例如在 Webpack 成功编译和输出了文件后执行发布操作把输出的文件上传到服务器。 同时该插件还能区分 Webpack 构建是否执行成功。使用该插件时方法如下:

module.exports = {
  plugins:[
    // 在初始化 EndWebpackPlugin 时传入了两个参数,分别是在成功时的回调函数和失败时的回调函数;
    new EndWebpackPlugin(() => {
      // Webpack 构建成功,并且文件输出了后会执行到这里,在这里可以做发布文件操作
    }, (err) => {
      // Webpack 构建失败,err 是导致错误的原因
      console.error(err);        
    })
  ]
}

要实现该插件,需要借助两个事件:

  • done:在成功构建并且输出了文件后,Webpack 即将退出时发生;
  • failed:在构建出现异常导致构建失败,Webpack 即将退出时发生;

实现该插件非常简单,完整代码如下:

class EndWebpackPlugin {

  constructor(doneCallback, failCallback) {
    // 存下在构造函数中传入的回调函数
    this.doneCallback = doneCallback;
    this.failCallback = failCallback;
  }

  apply(compiler) {
    compiler.plugin('done', (stats) => {
        // 在 done 事件中回调 doneCallback
        this.doneCallback(stats);
    });
    compiler.plugin('failed', (err) => {
        // 在 failed 事件中回调 failCallback
        this.failCallback(err);
    });
  }
}
// 导出插件 
module.exports = EndWebpackPlugin;

从开发这个插件可以看出,找到合适的事件点去完成功能在开发插件时显得尤为重要。 在 工作原理概括 中详细介绍过 Webpack 在运行过程中广播出常用事件,你可以从中找到你需要的事件。

调试 Webpack

在编写 Webpack 的 Plugin 和 Loader 时,可能执行结果会和你预期的不一样,就和你平时写代码遇到了奇怪的 Bug 一样。 对于无法一眼看出问题的 Bug,通常需要调试程序源码才能找出问题所在。

虽然可以通过 console.log 的方式完成调试,但这种方法非常不方便也不优雅,本节将教你如何断点调试 工作原理概括 中的插件代码。 由于 Webpack 运行在 Node.js 之上,调试 Webpack 就相对于调试 Node.js 程序。

在 Webstorm 中调试

Webstorm 集成了 Node.js 的调试工具,因此使用 Webstorm 调试 Webpack 非常简单。

1. 设置断点

在你认为可能出现问题的地方设下断点,点击编辑区代码左侧出现红点表示设置了断点。

2. 配置执行入口

告诉 Webstorm 如何启动 Webpack,由于 Webpack 实际上就是一个 Node.js 应用,因此需要新建一个 Node.js 类型的执行入口。

以上配置中有三点需要注意:

  • Name 设置成了 debug webpack,就像设置了一个别名,方便记忆和区分;
  • Working directory 设置为需要调试的插件所在的项目的根目录;
  • JavaScript file 即 Node.js 的执行入口文件,设置为 Webpack 的执行入口文件 node_modules/webpack/bin/webpack.js

3. 启动调试

经过以上两步,准备工作已经完成,下面启动调试,启动时选中前面设置的 debug webpack

4. 执行到断点

启动后程序就会停在断点所在的位置,在这里你可以方便的查看变量当前的状态,找出问题。

原理总结

Webpack 是一个庞大的 Node.js 应用,如果你阅读过它的源码,你会发现实现一个完整的 Webpack 需要编写非常多的代码。 但你无需了解所有的细节,只需了解其整体架构和部分细节即可。

对 Webpack 的使用者来说,它是一个简单强大的工具; 对 Webpack 的开发者来说,它是一个扩展性的高系统。

Webpack 之所以能成功,在于它把复杂的实现隐藏了起来,给用户暴露出的只是一个简单的工具,让用户能快速达成目的。 同时整体架构设计合理,扩展性高,开发扩展难度不高,通过社区补足了大量缺失的功能,让 Webpack 几乎能胜任任何场景。

通过本章的学习,希望你不仅能学会如何编写 Webpack 扩展,也能从中领悟到如何设计好的系统架构。

查看原文

赞 279 收藏 331 评论 21

达摩师兄 发布了文章 · 2月18日

从JavaScript中看设计模式(总结)

从JavaScript中看设计模式(总结)

本文原创文章,转载前请留言备注

概念

设计模式 (Design Pattern) 是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。

任何事情都有套路,设计模式就是写代码中常见的套路,有些写法我们日常都在使用,下面我们来介绍一下。

订阅/发布模式(观察者)

pub/sub这个应该大家用到的最广的设计 模式了

在这种模式中,并不是一个对象调用另一个对象的方法,而是一个对象订阅另一个对象

特定活动并在状态改变后获得通知,订阅者因此也成为观察者,而被观察的对象成为发布者或主题。当发生了一个重要事件的时候发布者会通知(调用)所有订阅者并且可能经常以事件对象的形式传递消息。

自己实现一个简单的发布订阅设计模式
// 创建EventBus
class EventBus {
  constructor() {
    // 储存事件
    this.tasks = {};
  }
  // 绑定事件
  $on(eName, cb) {
    typeof cb == "function"
      ? this.tasks[eName] || (this.tasks[eName] = [])
      : this.Error(cb, "is not a function");
    this.tasks[eName].some(fn => fn == cb) 
      ? true 
      : this.tasks[eName].push(cb); // 避免重复绑定
  }
  // 触发事件
  $emit(eName, ...arg) {
    let taskQueue;
    this.tasks[eName] && this.tasks[eName].length > 0
      ? (taskQueue = this.tasks[eName])
      : this.Error(eName, "is not defined or is a array of having empty callback");
    taskQueue.forEach(fn => {
      fn(...arg);
    });
  }
  // 触发一次
  $once(eName, cb) {
    let fn = (...arg) => {
      this.$off(eName, fn);
      cb(...arg);
    };
    typeof cb == "function" && this.$on(eName, fn);
  }
  // 卸载事件
  $off(eName, cb) {
    let taskQueue;
    this.tasks[eName] && this.tasks[eName].length > 0
      ? (taskQueue = this.tasks[eName])
      : this.Error(eName, "is not exist");
    if (typeof cb === "function") {
      let index = taskQueue.findIndex(v => (v == cb));
      index != -1 &&
        taskQueue.splice(
          taskQueue.findIndex(v => (v == cb)),
          1
        );
    }
    if (typeof cb === "undefined") {
      taskQueue.length = 0;
    }
  }
  // 异常处理
  Error(node, errorMsg) {
    throw Error(`${node} ${errorMsg}`);
  }
}

下面我们针对自己的模式进行简单的使用:

// 首先定义一个事件池
const EventSinks = {
  add(x, y) {
    console.log("总和: " + x + y);
  },
  multip(x, y) {
    console.log("乘积: " + x * y);
  },
  onceEvent() {
    console.log("我执行一次后就自动卸载");
  }
};

// 实例化对象
let bus = new EventBus();
bus.$on("operator", EventSinks.add); // 监听operator事件, 增加一个EventSinks.add
bus.$on("operator", EventSinks.add); // 当事件名和回调函数相同时,跳过,避免重复绑定
bus.$on("operator", EventSinks.multip); // 给operator事件增加一个EventSinks.multip回调函数
bus.$once("onceEvent", EventSinks.onceEvent); // 触发一次后卸载
console.log(bus.tasks); // { operator: [ [Function: add], [Function: multip] ], onceEvent: [ [Function: fn] ]}
bus.$emit("operator", 3, 5); // 总和:8  乘积:15
bus.$emit("onceEvent"); // 我就执行一次
console.log(bus.tasks); // { operator: [ [Function: add], [Function: multip] ], onceEvent: [] }
bus.$off("operator", EventSinks.add); // 卸载掉operator事件中的EventSinks.add函数体
console.log(bus.tasks); // { operator: [ [Function: multip] ], onceEvent: [] }
bus.$off("operator"); // 卸载operator事件的所有回调函数
console.log(bus.tasks); // { operator: [], onceEvent: [] }
bus.$emit("onceEvent"); // onceEvent is not defined or is a array of having empty callback

单例模式

单例模式的定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。实现的方法为先判断实例存在与否,如果存在则直接返回,否则就创建实例再返回,这就保证了一个类只实例化一次

使用场景:一个单一对象。比如:弹窗,无论点击多少次,弹窗只应该被创建一次,实现起来也很简单,用一个变量缓存起来即可。可以参考ElementUI模态框的实现

模仿一下单例模式(只要有个变量确保实例只创建一次)
class Singleton {
  constructor() {}
}

Singleton.getInstance = (function() {
  let instance
  return function() {
    if (!instance) {
      instance = new Singleton()
    }
    return instance
  }
})()

let s1 = Singleton.getInstance()
let s2 = Singleton.getInstance()
console.log(s1 === s2) // true

当我们再次创建时,如果实例化了,就不在实例化,直接返回,上面可以看出,二者相同

策略模式

策略模式的定义:定义一系列的算法,把他们一个个封装起来,并且使他们可以互相替换

策略模式的目的就是将算法的使用算法的实现分离出来

一个基于策略模式的程序至少由两部分组成。第一部分是一组策略类(可变),策略类封装了具体的算法,并负责具体的计算过程。第二部分是环境类Context(不变),Context接受客户的请求,随后将请求委托给某一个策略类。要做到这一点,说明Context中要维持对某个策略对象的引用

举个表单校验栗子:

// 普通写法
const form = document.querySelector("#form");
form.onsubmit = () => {
  if (form.username.value == "") {
    console.log("用户名不能为空");
    return false;
  }
  if(form.username.password.length < 10){
    console.log('密码长度不能小于10')
    return false
  }
}
简单的策略模式
// 创建校验器
const checker = {
  isEmpty(v, errorMsg){
    if(value === ''){
      return errorMsg
    }
  },
  minLength(v, length, errorMsg){
    if(value.length < length){
      return errorMsg
    }
  }
}
const validator = () => {
  // 校验规则存储
  this.cache = []
}
validator.prototype.add = () => {
  let arr = rule.split(':')
  this.cache.push(() => {
    let valit = arr.shift()
    arr.unshift(dom.value)
    arr.push(errorMsg)
    return checker[valit].apply(dom, arr)
  })
}
validator.prototype.start = () => {
  for(let i = 0, validatorFunc; validatorFunc = this.cache[i++];){
    // 开始校验,并取得校验后的返回值
    let msg = validatorFunc() 
    if(msg){
      return msg
    }
  }
}
const validatorFunc = () => {
  // 创建一个validator对象
  let valit = new validator() 
  valit.add(form.username, 'isEmpty', '用户名不能为空')
  valit.add(form.password, 'minLength', '密码长度不能小于10')
  // 获得校验结果
  let errorMsg = valit.start()  
  return errorMsg  
}

// 再次登录
const form = document.querySelector("#form");
form.onsubmit = () => {
  let errorMsg = validatorFunc()
  if(errorMsg){
    console.error(errorMsg)
    return false
  }
}

当创建校验器后,校验规则清晰明了,可以动态增改,便于维护

代理模式

代理模式的定义:为一个对象提供一个代用品或占位符,以便控制它的访问

常用的虚拟代理形式:某一个花销很大的操作,可以通过虚拟代理的方式延迟这种需要他的时候才去创建(例:使用虚拟代理实现图片懒加载)

图片懒加载的方式:先通过一张loading图占位,然后通过异步的方式加载图片,等图片加载好了再把请求成功的图片加载到img标签上

栗子:

const imgFunc = (() => {
    const imgNode = document.createElement('img')
    document.body.appendChild(imgNode)
    return{
        setSrc: function(src){
            imgNode.src = src
        }
    }
})()
const proxyImage = (() => {
    let img = new Image()
    img.onload = function(){
        imgFunc.setSrc(this.src)
    }
    return {
        setSrc: function(src){
            imgFunc.setSrc('./loading.gif')
            img.src = src
        }
    }
})()
proxyImage.setSrc('./pic.png')()

上面的栗子实现了加载图片时,在图片加载成功前,指定特定的图片,加载完成后替换成真是的数据

在我们生活中常用的事件代理、节流防抖函数其实都是代理模式的实现

装饰器模式

装饰器模式的定义:在不改变对象自身的基础上,在程序运行期间给对象动态地添加方法,注解也可以理解为装饰器。常见应用:react的高阶组件,或者react-redux中的@connect或者自己定义一些高阶组件

简单实现:

import React from 'react'
const withLog = Component => {
  // 完好无损渲染出来, 只是添加了两个生命周期函数
  class NewComponent extends React.Component{
    // 1
    componentWillMount(){ 
      console.time('ComponentRender')
      console.log('准备完毕了')
    }
    render(){  // 完好无损渲染出来
      return <Component { ...this.props }></Component>
    }
    // 2
    componentDidMount(){
      console.timeEnd('ComponentRender')
      console.log('渲染完毕了')
    }
  }
  return NewComponent
}
export { withLog }

@withLog
class xxx

在redux中可以找出装饰器的方式,其实Vue中的v-inputv-checkbox也可以认为是装饰器模式,对原生input和checkbox做一层装饰

装饰器模式和代理模式的结构看起来非常相似,这两种模式都描述了怎样为对象提供一定程度上的间接引用,并且向那个对象发送请求。代理模式和装饰器模式最重要的区别在于它们的意图和设计目的。代理模式的目的是:当直接访问本体不方便或者不符合需要时,为这个本体提供一个替代者。装饰模式目的是:为对象动态加入的行为,本体定义了关键功能,而装饰器提供或拒绝它的访问,或者在访问本体前做一些额外的事。

外观模式

外观模式的定义:即在内部让多个方法一起被调用

涉及到兼容性,参数支持多格式,有很多这种代码,对外暴露统一API,比如上面的$on支持数组,$off参数支持多种情况,对面只用一个函数,内部判断实现

举个简单的栗子:

// 封装一些事件,让其兼容各个浏览器
const myEvent = {
  stopBubble(e){
    if(typeof e.preventDefault() === 'function'){
      e.preventDefault()
    }
    if(typeof e.stopPropagation() === 'function'){
      e.stopPropagation()
    }
    // for IE
    if(typeof e.returnValue === 'boolean'){
      e.returnValue = false
    }
    if(typeof e.cancelBubble === 'boolean'){
      e.cancelBubble = false
    }
  },
  addEvent(dom, type, cb){
    if(dom.addEventListener){
      dom.addEventListener(type, cb, false)
    } else if(dom.attachEvent){
      dom.attachEvent('on' + type, cb)
    }else{
      dom['on' + type] = cb
    }
  }
}

以上就用外观模式封装了两个基本事件,让其兼容各种浏览器,调用者不需要知道内部的构造,只要知道这个方法怎么用就行了。

工厂模式

工厂模式的定义:提供创建对象的接口,把成员对象的创建工作转交给一个外部对象,好处就是消除对象直接的耦合(也就是相互影响)

常见的栗子,我们的弹窗message,对外部提供API,都是调用API,然后新建一个弹窗或者message的实例,就是典型的工程模式

简单的栗子:

class Man {
  constructor(name) {
    this.name = name
  }
  say(){
      console.log(`我的名字 ` + this.name)
  }
}
const p = new Man('JavaScript')
p.say() // 我的名字 JavaScript

当然工厂模式并不仅仅是用来 new 出实例

可以想象一个场景。假设有一份很复杂的代码需要用户去调用,但是用户并不关心这些复杂的代码,只需要你提供给我一个接口去调用,用户只负责传递需要的参数,至于这些参数怎么使用,内部有什么逻辑是不关心的,只需要你最后返回我一个实例。这个构造过程就是工厂。

再比如下面Vue这个例子:

const Notification = function(options) {
  if (Vue.prototype.$isServer) return;
  options = options || {};
  let userOnClose = options.onClose;
  let id = "notification_" + seed++;
  let postion = options.postion || "top-right";
  options.onClose = function() {
    Notification.close(id, userOnClose);
  };

  instance = new NotificationConstructor({
    data: options
  });
  if(isVNode(options.message)){
    instance.$slots.default = [options.message]
    options.message = 'REPLACED_BY_VNODE'
  }
  instance.id = id
  instance.$mount()
  document.body.appendChild(instance.$el)
  instance.visible = true
  instance.dom = instance.$el
  instance.dom.style.zIndex = PopupManager.nextZIndex()
  let verticalOffset = options.offset || 0
  instances.filter(item => {
    verticalOffset += item.$el.offsetHeight + 16
  })
  verticalOffset += 16
  instance.verticalOffset = verticalOffset
  instances.push(instance)
  return instance

};

在上述代码中,我们可以调用它封装好的方法就可以创建对象实例,至于它内部的实现原理我们并不关心。

建造者模式Builder

建造者模式的定义:和工厂者模式相比,参与了更多创建过程或者更加复杂
const Person = function(name, work){
  // 创建应聘者缓存对象
  let _person = new Human()

  // 创建应聘者姓名解析对象
  _person.name = new NamedNodeMap(name)

  // 创建应聘者期望职位
  _person.work = new Worker(work)

  return _person
}
const p = new Person('小明', 'Java')
console.log(p)

迭代器模式

迭代器模式定义:指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。迭代器模式可以把迭代的过程从业务逻辑中分离出来,在使用迭代器模式之后,即使不关心对象的内部构造,也可以按顺序访问其中的每个元素

比如常用的:every、map、filter、forEach等等

const each = function(arr, callback){
  if(!Array.isArray(arr)){
    throw Error(`${arr} is not a Array`)
  }
  for(let i = 0, l = arr.length; i < l; i++){
    callback.call(arr[i], i, arr[i])
  }
}
each([1,2,4], function(i, n){
  console.log([i, n])
})

享元模式

享元(flyweight)模式的定义:一种用于性能优化的模式,fly在这里是苍蝇的意思,意为蝇量级。享元模式的核心是运用共享技术来有效支持大量细粒度的对象。如果系统中因为创建了大量类似的对象而导致内存占用过高,享元模式就是非常有用了。在JavaScript中,浏览器特别是移动端的浏览器分配的内存并不多,如何节省内存就成了一件非常有意义的事情

假设有个内衣工厂,目前的产品有50中男衣和50中女士内衣,为了推销产品,工厂决定生产一些塑料模特来穿上他们的内衣拍成广告照片。正常情况下需要50个男模特和50个女模特,然后让他们每人分别穿上一件内衣来拍照

普通的做法:

const Model = function(sex, underwear){
  this.sex = sex
  this.underwear = underwear
}
Model.prototype.takePhoto = function(){
  console.log('sex=' + this.sex + ' underwear=' + this.underwear)
}
for(let i = 1; i <= 50; i++){
  let maleModel = new Model('male', 'underwear' + i)
  maleModel.takePhoto()
}
for(let join = 1; join <= 50; join++){
  let femaleModel = new Model('female', 'underwear' + join)
  femaleModel.takePhoto()
}

采用享元模式:

const Model = function(sex){
  this.sex = sex
}
Model.prototype.takePhoto = function(){
  console.log('sex=' + this.sex + ' underwear=' + this.underwear)
}
// 分别创建一个男模特和一个女模特对象
let maleModel = new Model('male'),
    femaleModel = new Model('female')
// 给男模特依次穿上所有的男装,并进行拍照
for(let i = 1; i <= 50; i++){
  maleModel.underwear = 'underwear' + i
  maleModel.takePhoto()
}
// 给女模特依次穿上所有的女装,并进行拍照
for(let j = 1; j <= 50; j++){
  femaleModel.underwear = 'underwear' + j
  femaleModel.takePhoto()
}
  • 内部状态存储于对象内部
  • 内部状态可以被一些对象共享
  • 内部状态独立于具体的场景,通常不会改变
  • 外部状态取决于具体的场景,并根据场景而变化,外部状态不能被共享

职责链模式

职责链模式的定义:使多个对象都有机会处理请求,从而避免请求发送者和接受者之间的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,知道有一个对象处理它为止。职责链模式的名字非常形象,一系列可能会处理请求的对象被连成一条链,请求在这些对象之间依次传递,知道遇到一个可以处理它的对象,我们把这些对象称为链中的节点

简单的栗子:假设我们负责一个售卖手机的电商网站,分别经过缴纳500元定金和200元定金的两轮预定后(订单已在此时生成),现在已经到了正式购买的阶段。公司针对支付过预定金的用户有一定的优惠政策。在正式购买后,已经支付过500元定金的用户会受到100元的商城优惠券,200元定金的用户可以收到50元的优惠券,而之前没有支付定金的用户只能进入普通购买模式,也就是没有优惠券,且在存库有限的情况下不一定保证买到

let order500 = function(orderType, pay, stock) {
  if (orderType === 1 && pay === true) {
    console.log("500元定金预购,得到100元优惠券");
  } else {
    // 我不知道下一个节点是谁,反正把请求往后面传递
    return "nextSuccessor";
  }
};
let order200 = function(orderType, pay, stock) {
  if (orderType === 2 && pay === true) {
    console.log("200元定金预购,得到50元优惠券");
  } else {
    return "nextSuccessor";
  }
};
let orderNormal = function(orderType, pay, stock) {
  if (stock > 0) {
    console.log("普通购买, 无优惠券");
  } else {
    console.log("库存不足");
  }
};
let Chain = function(fn) {
  this.fn = fn;
  this.successor = null;
};
// Chain.prototype.setNextSuccessor 指定在链中的下一个节点
Chain.prototype.setNextSuccessor = function(successor) {
  return (this.successor = successor);
};
// Chain.prototype.passRequest 传递请求给某个节点
Chain.prototype.passRequest = function() {
  let ret = this.fn.apply(this, arguments);
  if (ret === "nextSuccessor") {
    return (
      this.successor &&
      this.successor.passRequest.apply(this.successor, arguments)
    );
  }
  return ret;
};
let chainOrder500 = new Chain(order500)
let chainOrder200 = new Chain(order200)
let chainOrderNormal = new Chain(orderNormal)

chainOrder500.setNextSuccessor(chainOrder200)
chainOrder200.setNextSuccessor(chainOrderNormal)
// 500元定金预购,得到100元优惠券
chainOrder500.passRequest(1, true, 500)
// 200元定金预购,得到50元优惠券
chainOrder500.passRequest(2, true, 500)
// 普通购买,无优惠券
chainOrder500.passRequest(3, true, 500)
// 库存不足
chainOrder500.passRequest(1, false, 0)

适配器模式

适配器模式定义:解决两个软件实体间的接口不兼容的问题。使用适配器模式之后,原本由于接口不兼容而不能工作的两个软件实体可以一起工作。适配器的别名是包装器(wrapper),这是一个相对简单的模式。在程序开发过程中有许多这样的场景:当我们试图调用模块或者对象的某个接口时,却发现这个接口的格式并不符合目前需求。这时候有两种解决办法,第一种是修改原来的接口实现,但如果原来的模板很复杂,或者我们拿到模块是一段别人编写的经过压缩的代码,修改原接口就显得不太现实了。第二种方法是创建一个适配器,将原接口转换为客户希望的另一个接口,客户只需要和适配器打交道
let googleMap = {
  show: function(){
    console.log('开始渲染谷歌地图')
  }
}
let baiduMap = {
  display: function(){
    console.log('开始渲染百度地图')
  }
}
let baiduMapAdapter = {
  show: function(){
    return baiduMap.display()
  }
}
renderMap(googleMap)  // 开始渲染谷歌地图
renderMap(baiduMapAdapter)  // 开始渲染百度地图

适配器模式主要用来解决两个已有接口不匹配的问题,它不考虑这接口时怎么实现的,也不考虑他们将来可能会如何演化。适配器模式不需要改变已有的接口,就能够使他们协同作用

装饰模式和代理模式也不会改变原有对象的接口,但装饰器模式的作用是为了给对象增加功能。装饰器模式常常形成一条长的装饰链,适配器模式通常只包装一次。代理模式为了控制对对象的访问,通常也只包装一次。

我们设计很多插件,有默认值,也算是适配器的一种应用,vue的prop校验,default也算是适配器的应用了

外观模式的作用倒是和适配器比较相似,有人把外观模式看成一组对象的适配器,但外观模式最显著的特点是定义了一个新的接口。

模板方法模式

模板方法模式定义:在一个方法中定义一个算法骨架,而将一些步骤的实现延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中某些步骤的具体实现

我们常用的有很多,vue中的slot,react中的children

class Parent {
  constructor() {}
  render() {
    <div>
      <div name="tom"></div>
      {/* 算法过程:children要渲染在name为joe的div中 */}
      <div name="joe">{this.props.children}</div>
    </div>
  }
}
class Stage{
  constructor(){}
  render(){
    // 在parent中已经设定了children的渲染位置算法
    <Parent>
      // children的具体实现
      <div>child</div>
    </Parent>
  }
}
<template>
  <div>
    <div name="tom"></div>
    <div name="joe">
      <slot />
    </div>
  </div>
</template>
<template>
  <div>
    <parent>
      <!-- children的具体实现 -->
      <div>child</div>
    </parent>
  </div>
</template>

中介者模式

中介者模式的定义:通过一个中介者对象,其他所有的相关对象都通过该中介者来通信,而不是相互引用,当其中的一个对象发生改变时,只需要通知中介者对象即可。通过中介者模式可以解除对象与对象之间的紧耦合关系(目的就是减少耦合)

栗子:现实生活中,航线上的飞机只需要和机场的塔台通信就能确定航线和飞行状态,而不需要和所有飞机通信。同时塔台作为中介者,知道每架飞机的飞行状态,所以可以安排所有飞机的起降和航信安排。

中介者模式使用场景:例如购物车需求,存在商品选择表单、颜色选择表单、购买数量表单等等,都会触发change事件,那么可以通过中介者来转发处理这些事件,实现各个事件间的解耦,仅仅维护中介者对象即可。

redux、vuex都属于中介者模式的实际应用,我们把共享的数据,抽离成一个单独的store,每个都通过tore这个中介者来操作对象

备忘录模式

备忘录模式定义:可以恢复到对象之前的某个状态,其实大家学习react或者redux的时候,时间旅行的功能,就算是备忘录模式的一个应用
​```

推荐设计模式书籍

总结

创建设计模式:工厂,单例、建造者、原型

结构化设计模式:外观,适配器,代理,装饰器,享元,桥接,组合

行为型模式:策略、模板方法、观察者、迭代器、责任链、命令、备忘录、状态、访问者、终结者、解释器

点击这里阅读原文

以上如有错误,欢迎纠正

查看原文

赞 1 收藏 1 评论 0

达摩师兄 收藏了文章 · 2019-11-24

前端最实用书签(持续更新)

前言

一直混迹社区,突然发现自己收藏了不少好文但是管理起来有点混乱;
所以将前端主流技术做了一个书签整理,不求最多最全,但求最实用。

书签源码

书签导入浏览器效果截图
书签

书签源码请戳github地址
Chrome---设置---书签---导入书签就可在浏览器即可使用

1.ES6,ES7和ES8

2.vue

2.1 api

2.2 vue源码

2.3 插件

2.3 项目

2.4 vue-cli

2.5 vue高级

3.react

3.1 api

3.2 ui组件

3.3 项目

3.4 源码解析

4.微信小程序

4.1 api

4.2 插件

4.3 项目

电商项目1
小程序demo

5.Hybrid

5.1 通讯

5.2 适配和兼容

6.webpack

7.npm

npm配置

8.node.js

8.1 api

8.2 框架

8.3项目

9.高德地图

9.css

10.性能优化

11.安全

12.前端架构

13.算法

14.前端测试

15.常用工具

16.浏览器

17.大佬博客

18.面试

结语

后期会继续优化维护这些书签
为了便于知识归纳,所以将有些作者的文章标题修改了,谢谢优秀作者的文章分享!

查看原文

达摩师兄 赞了文章 · 2019-11-24

前端最实用书签(持续更新)

前言

一直混迹社区,突然发现自己收藏了不少好文但是管理起来有点混乱;
所以将前端主流技术做了一个书签整理,不求最多最全,但求最实用。

书签源码

书签导入浏览器效果截图
书签

书签源码请戳github地址
Chrome---设置---书签---导入书签就可在浏览器即可使用

1.ES6,ES7和ES8

2.vue

2.1 api

2.2 vue源码

2.3 插件

2.3 项目

2.4 vue-cli

2.5 vue高级

3.react

3.1 api

3.2 ui组件

3.3 项目

3.4 源码解析

4.微信小程序

4.1 api

4.2 插件

4.3 项目

电商项目1
小程序demo

5.Hybrid

5.1 通讯

5.2 适配和兼容

6.webpack

7.npm

npm配置

8.node.js

8.1 api

8.2 框架

8.3项目

9.高德地图

9.css

10.性能优化

11.安全

12.前端架构

13.算法

14.前端测试

15.常用工具

16.浏览器

17.大佬博客

18.面试

结语

后期会继续优化维护这些书签
为了便于知识归纳,所以将有些作者的文章标题修改了,谢谢优秀作者的文章分享!

查看原文

赞 723 收藏 611 评论 11

达摩师兄 发布了文章 · 2019-11-23

用Vuepress搭建博客?看这篇就完事了,轻松玩转Vuepress

作者博客地址:管鲍切思世所稀的博客... , 移动端和PC端有差别哦~

前几天看到掘金上有人用Vuepress搭建博客,知道vuepress的我,却从来没有亲自上手过,于是乎瞎琢磨一下。总结:vuepress还是很强的,操作简单,没有那么难,只要你认真阅读了文章,你可以做vuepress的爸爸了。这篇文章通俗易懂,从起步到结束,按顺序讲解,认真看完你就是最靓的仔~

关于笔者博客

本博客是基于vuepress搭建而成,什么是vuepress? => vuepress是以Vue驱动的静态网站生成器。VuePress 由两部分组成:第一部分是一个极简静态网站生成器,它包含由 Vue 驱动的主题系统和插件 API,另一个部分是为书写技术文档而优化的默认主题,它的诞生初衷是为了支持 Vue 及其子项目的文档需求。

提示(认真看完你就是最靓的仔~)

你可能需要30-50分钟时间读完它,才能胜任vuepress

Vuepress三大特点:

  • 简洁至上: 以 Markdown 为中心的项目结构,以最少的配置帮助你专注于写作。
  • Vue 驱动: 享受 Vue + webpack 的开发体验,可以在 Markdown 中使用 Vue 组件,又可以使用 Vue 来开发自定义主题。
  • 高性能: VuePress 会为每个页面预渲染生成静态的 HTML,同时,每个页面被加载的时候,将作为 SPA 运行。

介绍

vuepress上手成本像 1,2,3一样容易

# 安装
yarn global add vuepress # 或者:npm install -g vuepress

# 新建一个 markdown 文件
echo '# Hello VuePress!' > README.md

# 开始写作
vuepress dev .

# 构建静态文件
vuepress build .
注意:请确保你的 Node.js 版本 >= 8.6。

它是如何工作的?

事实上,一个 VuePress 网站是一个由 VueVue Routerwebpack 驱动的单页应用。如果你以前使用过 Vue 的话,当你在开发一个自定义主题的时候,你会感受到非常熟悉的开发体验,你甚至可以使用 Vue DevTools 去调试你的自定义主题。

在构建时,我们会为应用创建一个服务端渲染(SSR)的版本,然后通过虚拟访问每一条路径来渲染对应的HTML。这种做法的灵感来源于 Nuxtnuxt generate 命令,以及其他的一些项目,比如 Gatsby

官方话就不扯了,更多官方术语详情请移步官方

快速上手

请确保你的 Node.js 版本 >= 8。

全局安装

如果你只是想尝试一下 VuePress,你可以全局安装它:

# 安装
yarn global add vuepress # 或者:npm install -g vuepress

# 新建一个 markdown 文件
echo '# Hello VuePress!' > README.md

# 开始写作
vuepress dev .

# 构建静态文件
vuepress build .

现有项目

如果你想在一个现有项目中使用 VuePress,同时想要在该项目中管理文档,则应该将 VuePress 安装为本地依赖。作为本地依赖安装让你可以使用持续集成工具,或者一些其他服务(比如 Netlify)来帮助你在每次提交代码时自动部署。

# 将 VuePress 作为一个本地依赖安装
yarn add -D vuepress # 或者:npm install -D vuepress

# 新建一个 docs 文件夹
mkdir docs

# 新建一个 markdown 文件
echo '# Hello VuePress!' > docs/README.md

# 开始写作
npx vuepress dev docs
注意如果你的现有项目依赖了 webpack 3.x,推荐使用 Yarn 而不是 npm 来安装 VuePress。因为在这种情形下,npm 会生成错误的依赖树。

接着,在 package.json 里加一些脚本:

{
  "scripts": {
    "docs:dev": "vuepress dev docs",
    "docs:build": "vuepress build docs"
  }
}

然后就可以开始写作了:

yarn docs:dev # 或者:npm run docs:dev

要生成静态的 HTML 文件,运行:

yarn docs:build # 或者:npm run docs:build

默认情况下,文件将会被生成在 .vuepress/dist,当然,你也可以通过 .vuepress/config.js 中的 dest 字段来修改,生成的文件可以部署到任意的静态文件服务器上,参考 部署 来了解更多。

目录结构

VuePress 遵循 “约定优于配置” 的原则,推荐的目录结构如下:

注意vuepress约定入口目录必须是 docs ,务必根目录是docs
.
├── docs
│   ├── .vuepress (可选的)  →  官方标注可选,不过一般都会用这个文件夹,核心文件夹
│   │   ├── components (可选的)  →  这个文件夹一些以.vue结尾的vue组件,可以在markdown文件里使用
│   │   ├── theme (可选的)   →  可以配置自己的博客
│   │   │   └── Layout.vue
│   │   ├── public (可选的)   →   放一些公共静态资源 使用方式 /xxx, 请必须以 `/` 开始表示根
│   │   ├── styles (可选的)  →  样式
│   │   │   ├── index.styl   →  自定义样式
│   │   │   └── palette.styl   →  用于重写默认颜色常量,或者设置新的 stylus 颜色常量
│   │   ├── templates (可选的, 谨慎配置)
│   │   │   ├── dev.html  →  用于开发环境的 HTML 模板文件
│   │   │   └── ssr.html  →  构建时基于 Vue SSR 的 HTML 模板文件
│   │   ├── config.js (可选的)   →   配置文件的入口文件,也可以是 YML 或 toml
│   │   └── enhanceApp.js (可选的)   →  客户端应用的增强
│   │ 
│   ├── README.md
│   ├── guide
│   │   └── README.md
│   └── config.md
│ 
└── package.json
注意当你想要去自定义 templates/ssr.html 或 templates/dev.html 时,最好基于 默认的模板文件 来修改,否则可能会导致构建出错。

默认的页面路由

此外,对于上述的目录结构,默认页面路由地址如下:

文件的相对路径页面路由地址
/README.md/
/guide/README.md/guide/
/config.md/config.html

基本配置

// dcos/.vuepress/config.js
module.exports = {
    title:"取舍",   // HTML的title
    description:"管鲍切思世所稀博客",   // 描述
    keywords:"管鲍切思世所稀博客",  // 关键字
    head:[   // 配置头部
        [
            ['link', {rel:'icon', href:"/icon.png"}],
            ['meta', {'name':'viewport', content:"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0;"}]
        ]
    ],
    markdown: {
        lineNumbers: true,  // 代码显示行号
    }, 
    dest:"./outer",    // 设置打包路径
    lastUpdated: 'Last Updated',    // 显示更新时间
    themeConfig:{
        logo:"/icon.png",   // 导航栏左边logo,不写就不显示
        sidebarDepth: 2, // 侧边栏显示2级
        nav:[   // 导航栏配置
            { text: 'vue', link: '/' },
            { text: 'css', link: '/blog/' },
            { 
                text: 'js',  // 这里是下拉列表展现形式。 items可以一直嵌套下去
                items:[
                    text:"ES5", link:"/js/es5/",
                    text:"ES6", link:"/js/es6/",
                ]
            },
            {text: 'github', link:"https://github.com/1046224544"}
        ],
        // 为以下路由添加侧边栏
        sidebar: ['/', '/git', '/vue']
        // 嵌套侧边栏  以对象的方式嵌套下去
        // sidebar: {
        //     '/2019/': [
        //         ['','前言(2019)'],
        //         {
        //             title:"10月份",
        //             collapsable:false,
        //             sidebarDepth:2,
        //             children:[
        //                 ["Nginx部署Vue项目", "Nginx部署Vue项目"],
        //                 ["NVM自由切换Node版本", "NVM自由切换Node版本小笔记"],
        //                 ["KTV点歌系统", "KTV点歌系统"],
        //             ]
        //         },
        //         {
        //             title:"9月份",
        //             collapsable:false,
        //             sidebarDepth:2,
        //             children:[
        //                 ["综合性博客网站", "综合性博客网站"]
        //             ]
        //         }
        //     ],
        //     ...
        // }
    },
    plugins:[
        // 'axios'  // 配置插件
    ]
  }
}
注意以上就是完整的基本config.js配置文件,有强迫症的同学肯定会觉得文件大了,不好维护。的确,当我们的路有文件变多了,文件会变得很长。笔者在这里推荐,将导航栏侧边栏单独拆分两个文件,以文件加载的方式引入,这样路由导航栏侧边栏就可以无限嵌套,你也可以无限往下分级,建文件夹等等达到分类效果。

如下:

// docs/.vuepress/config.js
module.exports = {
    ...部分(同上)
    themeConfig:{
        nav: require('./nav'),   // 引入导航栏
        sidebar:require('./sidebar'),  // 引入侧边栏
    },
    ...
}


//  docs/.vuepress/sidebar.js
module.exports = {
    "/api/front/2019/": require('../.vuepress/frontbar/2019'),  // 继续分类
    "/api/front/2020/": require('../.vuepress/frontbar/2020'),
    "/api/end/2019/": require('../.vuepress/endbar/2019'),
    "/api/learn/koa/": require('../.vuepress/learnbar/koabar'),
    "/api/learn/express/": require('../.vuepress/learnbar/expressbar'),
    "/api/learn/java/": require('../.vuepress/learnbar/javabar'),
    "/api/learn/es6/": require('../.vuepress/learnbar/es6bar'),
    "/api/learn/vue/": require('../.vuepress/learnbar/vuebar'),
}


//   docs/.vuepress/nav.js
module.exports = [
    {text:"首页", link:"/"},
    {
        text:"技术API",
        ariLabel:"技术API",
        items:[
            {text:"koa", link:"/api/learn/koa/"},
            {text:"vue", link:"/api/learn/vue/"},
            {text:"es6", link:"/api/learn/es6/"},
            {text:"java", link:"/api/learn/java/"},
            {text:"express", link:"/api/learn/express/"},
        ]
    },
    {
        text:"日常博客",
        ariLabel:"日常博客",
        items:[
            {text:"前端",link:"/api/front/"},
            {text:"后端",link:"/api/end/"},
            {text:"其他",link:"/api/orther/1.md"},
        ]
    },
    {text:"关于博客", link:"/api/builog/"},
    {text:"关于作者", link:"/api/author/"},
    {
        text:"其他小站",
        ariLabel:"其他小站", 
        items:[
            {text:"掘金", link:'https://juejin.im/user/5d1079ab6fb9a07ed4410cc0'},
            {text:"SegmentFault", link:'https://segmentfault.com/u/98kk'},
            {text:"CSDN", link:'https://blog.csdn.net/weixin_43374176'},
        ]
    },
    {
        text:"联系",
        ariLabel:"联系",
        items:[
            {text:"邮箱", link:"mailto:wsm_1105@163.com", target:"_blank"},
            {text:"其他", link:"/api/contact/"}
        ]
    },
    {text:"GitHub", link:"http://github.com/1046224544"}
]

笔者目录截图:

注意只要是自己配置的目录,都必须在docs目录下有对应的目录结构, 如果你的路径是 /api/aboutme/, 那么对象的文件目录就是 /docs/api/aboutme/readme.md,默认根路径就是readme.md文件。 如果你的路径是 /api/aboutme/ktv点歌系统, 那么对象的文件目录就是 /docs/api/aboutme/ktv点歌系统.md

在 Vue文件中使用第三方库(两步走)

  1. 在 VuePress 中编写 Vue 代码,和我们通常的编写单文件的方式一致,有些时候我们有可能需要使用 Vue 的 UI 组件库。例如 ElementMint 等,通常我们在项目中使用这些 UI 组件库的时候,我们都会在 main.jsbotostrap.js 文件中统一注册。好在 VuePress 中也支持这种功能,我们可以通过创建一个 .vuepress/enhanceApp.js 文件来做一些应用级别的配置,这个文件 exprot default 一个钩子函数,在这个钩子中你可以做一些特殊处理,例如添加全局路由钩子,注册外部组件库。
  2. 在vue文件中像平时一样调用它
// .vuepress/enhanceApp.js
// 全局注册 Element 组件库
import Vue from 'vue'
import Element from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'

export default ({
  Vue,
  options,
  router
}) => {
  Vue.use(Element)
}

// 在vue中使用第三方库
// 定义一个vue组件   /docs/.vuepress/componets/demo.vue
<template>
  <div class="demo">
    {{ msg }}
    <my-hello></my-hello>
    <el-button>button</el-button>   // 使用了el-button
  </div>
</template>

<script>
export default {
  data () {
    return {
      msg: 'Hello VuePress!'
    }
  }
}
</script>

在markdown文件中使用vue组件

请确保只在 beforeMount 或者 mounted 访问浏览器 / DOM 的 API。
如果你正在使用,或者需要展示一个对于 SSR 不怎么友好的组件(比如包含了自定义指令),你可以将它们包裹在内置的 <ClientOnly> 组件中:

<ClientOnly>
    <demo />
</ClientOnly>

请注意,这并不能解决一些组件或库在导入时就试图访问浏览器 API 的问题 —— 如果需要使用这样的组件或库,你需要在合适的生命周期钩子中动态导入它们:

<script>
export default {
  mounted () {
    import('./lib-that-access-window-on-import').then(module => {
      // use code
    })
  }
}
</script>

如果你的模块通过 export default 导出一个 Vue 组件,那么你可以动态注册它:

<template>
  <component v-if="dynamicComponent" :is="dynamicComponent"></component>
</template>
<script>
export default {
  data() {
    return {
      dynamicComponent: null
    }
  },
  mounted () {
    import('./lib-that-access-window-on-import').then(module => {
      this.dynamicComponent = module.default
    })
  }
}
</script>

参考:

Markdown 拓展

链接

内部链接

网站内部的链接,将会被转换成 <router-link> 用于 SPA 导航。同时,站内的每一个文件夹下的 README.md 或者 index.md 文件都会被自动编译为 index.html,对应的链接将被视为 /
以如下的文件结构为例:

.
├─ README.md
├─ foo
│  ├─ README.md
│  ├─ one.md
│  └─ two.md
└─ bar
   ├─ README.md
   ├─ three.md
   └─ four.md

假设你现在在 foo/one.md 中:

[Home](/) <!-- 跳转到根部的 README.md -->
[foo](/foo/) <!-- 跳转到 foo 文件夹的 index.html -->
[foo heading](./#heading) <!-- 跳转到 foo/index.html 的特定标题位置 -->
[bar - three](../bar/three.md) <!-- 具体文件可以使用 .md 结尾(推荐) -->
[bar - four](../bar/four.html) <!-- 也可以用 .html -->

链接的重定向

VuePress 支持重定向到干净链接。如果一个链接 /foo 找不到,VuePress 会自行寻找一个可用的 /foo//foo.html。反过来,当 /foo//foo.html 中的一个找不到时,VuePress 也会尝试寻找另一个。借助这种特性,我们可以通过官方插件 vuepress-plugin-clean-urls 定制你的网站路径。

注意无论是否使用了 permalink 和 clean-urls 插件,你的相对路径都应该依赖于当前的文件结构来定义。在上面的例子中,即使你将 /foo/one.md 的路径设为了 /foo/one/,你依然应该通过 ./two.md 来访问 /foo/two.md

外部链接

外部的链接将会被自动地设置为 target="_blank" rel="noopener noreferrer":

你可以自定义通过配置 config.markdown.externalLinks 来自定义外部链接的特性

Front Matter

VuePress 提供了对 YAML front matter 开箱即用的支持:

---
title: Blogging Like a Hacker
lang: en-US
---

这些数据可以在当前 markdown 的正文,或者是任意的自定义或主题组件中使用。

想了解更多,请移步 Front Matter

GitHub 风格的表格

输入

| Tables        | Are           | Cool  |
| ------------- |:-------------:| -----:|
| col 3 is      | right-aligned | $1600 |
| col 2 is      | centered      |   $12 |
| zebra stripes | are neat      |    $1 |

输出

TablesAreCool
col 3 isright-aligned$1600
col 2 iscentered$12
zebra stripesare neat$1

Emoji

输入

:tada: :100:

输出

你可以在这个列表找到所有可用的 Emoji。

目录

输入

[[toc]]

输出

目录(Table of Contents)的渲染可以通过 markdown.toc 选项来配置。

自定义容器

输入

::: tip
This is a tip
:::

::: warning
This is a warning
:::

::: danger
This is a dangerous warning
:::

输出
::: tip
This is a tip
:::

::: warning
This is a warning
:::

::: danger
This is a dangerous warning
:::

你也可以自定义块中的标题:

::: danger STOP
Danger zone, do not proceed
:::

::: danger STOP
Danger zone, do not proceed
:::

参考:

代码块中的语法高亮

VuePress 使用了 Prism 来为 markdown 中的代码块实现语法高亮。Prism 支持大量的编程语言,你需要做的只是在代码块的开始倒勾中附加一个有效的语言别名:
输入

export default {
name: 'MyComponent',
// ...
}
```

**输出**

export default {
name: 'MyComponent',
// ...
}

**输入**
<ul>
<li
    v-for="todo in todos"
    :key="todo.id"
>
    {{ todo.text }}
</li>
</ul>

**输出**

<ul>
<li

v-for="todo in todos"
:key="todo.id"
{{ todo.text }}

</li>
</ul>


## 代码块中的行高亮

**输入**
export default {
data () {
    return {
    msg: 'Highlighted!'
    }
}
}
**输出**

export default {
data () {

return {
  msg: 'Highlighted!'
}

}
}


## 行号

你可以通过配置来为每个代码块显示行号:

module.exports = {
markdown: {

lineNumbers: true

}
}


## 导入代码段

你可以通过下述的语法导入已经存在的文件中的代码段:

<<< @/filepath

它也支持 行高亮:

<<< @/filepath{highlightLines}


>**注意**由于代码段的导入将在 webpack 编译之前执行,因此你无法使用 webpack 中的路径别名,此处的 `@` 默认值是 `process.cwd()`。


## 进阶配置
VuePress 使用 [markdown-it](https://github.com/markdown-it/markdown-it) 来渲染 Markdown,上述大多数的拓展也都是通过自定义的插件实现的。想要进一步的话,你可以通过 `.vuepress/config.js` 的` markdown` 选项,来对当前的 `markdown-it` 实例做一些自定义的配置:

module.exports = {
markdown: {

// markdown-it-anchor 的选项
anchor: { permalink: false },
// markdown-it-toc 的选项
toc: { includeLevel: [1, 2] },
extendMarkdown: md => {
  // 使用更多的 markdown-it 插件!
  md.use(require('markdown-it-xxx'))
}

}
}


## 评论系统 Valine

>看完本节内容,你就可以胜任Valine了,从此网站不在寂静,本评论系统通俗易懂,完全可以解决其他作者讲解此部分留下的后遗症,赶紧收藏吧!

- 需要安装leancloud-storage和valine
- [valine官网](https://valine.js.org/quickstart.html)
- appid和appkey的获取需要注册[leancloud](https://leancloud.cn/),然后可以新建应用并在`设置>应用 key`可以查看


### 安装

Install leancloud's js-sdk

npm install leancloud-storage --save

Install valine

npm install valine --save


### 配置
Components.vue组件

// 新建Components.vue组件
<template>

<div class="vcomment" v-if="data.comments === true">
  <div id="vcomments"></div>
</div>

</template>

<script>
import { isActive, hashRE, groupHeaders } from '../util'
export default {
computed: {

data () {
  return this.$page.frontmatter
},

},
mounted: function(){

this.createValine()

},

methods: {

createValine() {
  const Valine = require('valine');
  window.AV = require('leancloud-storage');
  const valine =  new Valine({
    el: '#vcomments',
    appId: 'your ID',
    appKey: 'your Key',
    notify: false,
    verify: false,
    avatar: 'monsterid',
    path: window.location.pathname,
    placeholder: '同道中人,文明留言...',
  });
  this.valineRefresh = false
}

},
watch: {

'$route' (to, from) {
  if(to.path !==  from.path){
    setTimeout(() => {
      //重新刷新valine
      this.createValine()
    }, 180)
  }
}

}
}
</script>

<style lang="stylus" rel="stylesheet/stylus">

vcomments {

max-width 740px
padding 10px
display block;
margin-left auto;
margin-right auto;
}
</style>


Page.vue

// Page.vue
<template>
<main class="page">

<slot name="top" />

<Content class="theme-default-content" />
<PageEdit />

<PageNav v-bind="{ sidebarItems }" />

<slot name="bottom" />
<Comments v-bind:is="viewComments"></Comments>

</main>
</template>

<script>
import PageEdit from '@theme/components/PageEdit.vue'
import PageNav from '@theme/components/PageNav.vue'
import { resolvePage, normalize, outboundRE, endingSlashRE } from '../util'
import Comments from './Comments.vue'

export default {
components: { PageEdit, PageNav , Comments},
props: ['sidebarItems'],
data(){

return{
  viewComments: 'Comments',
}

}
}
</script>

<style lang="stylus">
@require '../styles/wrapper.styl'

.page
padding-bottom 2rem
display block

</style>


### 使用(按需使用)
比如你想在china.md 文件中使用, 你就在头部 贴入一下代码
评论系统不是每个页面都一样的,根据不同的页面,加载不同的评论,
看了很多小编说什么评论的乱七八糟,看完这篇文章,对Valine不再那么恐慌,让你玩转第三方评论

// China.md

coments: true


**截图**

![](https://user-gold-cdn.xitu.io/2019/11/23/16e974512e5203a8?w=1736&h=919&f=png&s=125627)

![](https://user-gold-cdn.xitu.io/2019/11/23/16e974657170e175?w=1914&h=1078&f=png&s=161457)

>Valine实例与leancloud-storage实例 在每次页面加载时会向服务器发起带当前url参数的请求以获取评论数据,而这个url参数每次都是一样。首先Valine 实例与 leancloud-storage 实例都在 mounted 钩子中初始化或挂载至 window 对象上了。当页面 url 变化时,Page.vue 本身并没有变化,但mounted会重新触发。


## 关于其他插件
你可以移步到官网查看,插件就是给我们用的,如果难的话,还叫什么插件,只要认真看,你总会看懂的

## 部署到 Github pages
当我们将文档写好后就到了我们最关心的地方了,怎么将打包后的代码推送到远程仓库的 `gh-pages` 分支上,网上应该有很多文章描述怎么做,但是很多方法比较麻烦,还好有工具已经为我们解决了这个麻烦了。

### 创建一个deploy.sh

touch deploy.sh


### 编写脚本

!/usr/bin/env sh

确保脚本抛出遇到的错误

set -e

生成静态文件

npm run docs:build

进入生成的文件夹

cd docs/.vuepress/dist

如果是发布到自定义域名

echo 'www.example.com' > CNAME

git init
git add -A
git commit -m 'deploy'

如果发布到 https://<USERNAME>.github.io

git push -f git@github.com:<USERNAME>/<USERNAME>.github.io.git master

如果发布到 https://<USERNAME>.github.io/<REPO>

git push -f git@github.com:<USERNAME>/<REPO>.git master:gh-pages

cd -

### 设置package.json

{

"scripts": {
    "deploy": "bash deploy.sh"
  },

}

### 发布

npm run deploy // 即可自动构建部署到github上。



**详情移步官网**
- [vuepress.vuejs.org](https://vuepress.vuejs.org/zh/guide/deploy.html)

## 总结
相比较 Hexo 而言 VuePress 上手更加容易,功能也更强大,例如在 VuePress 可以注册自定义组件,而且 VuePress 中编写 Vue 和平时一样学习成本几乎为零。所以如果您正在开源一款 Vue 相关的库或是其他项目,您都可以使用 VuePress 作为您的文档编辑工具。虽然并没有完全将 VuePress 内容讲完,学完该篇文章相信你可以对 VuePress 有个大概的了解,您至少可以快速搭建一个博客,如果您想对 VuePress 有更多了解,请参考 [Vuepress 中文 API](https://vuepress.vuejs.org/zh/)

## 在线查看
[管鲍切思世所稀的博客...](https://blog.usword.cn/api/builog/)

>移动端和PC端有差别哦~




## 博客部分截图

![](https://user-gold-cdn.xitu.io/2019/11/23/16e9748ca35656c7?w=1738&h=920&f=png&s=112208)

![](https://user-gold-cdn.xitu.io/2019/11/23/16e97497621d5dc1?w=1741&h=918&f=png&s=117851)

![](https://user-gold-cdn.xitu.io/2019/11/23/16e974a11bfd972d?w=1739&h=919&f=png&s=137144)

## 其他
- [完整的综合性网站](https://juejin.im/post/5da2a8ed6fb9a04de818eeff)
- [KTV点歌系统](https://juejin.im/post/5dac3b4351882576534d33d7)
- [Nginx部署Vue项目](https://juejin.im/post/5dbbb4df51882522c14f81a8)
- [NVM的使用小技巧](https://juejin.im/post/5dae55b75188257d8936be94)

## 联系
如果大家有兴趣,欢迎关注公众号:_facebook(web技术进阶),查看更多优质文章,欢迎大家加入我的前端交流群:[866068198](https://qm.qq.com/cgi-bin/qm/qr?k=m8asYpHTs_cw3lJVLVv4U6wbF4ep2Ny5&authKey=OknnnniiEOFaIZNhEl2dUIhSrKPB8wGrEYu1AGyS01Y6XXz7doQl7v%2FitvPRr3ii) ,一起交流学习前端技术。博主目前一直在自学Node中,技术有限,如果可以,会尽力给大家提供一些帮助,或是一些学习方法.
- 公众号
![](https://user-gold-cdn.xitu.io/2019/11/23/16e974db11404fe7?w=748&h=776&f=jpeg&s=54595)

- QQ群

<a href="https://qm.qq.com/cgi-bin/qm/qr?k=m8asYpHTs_cw3lJVLVv4U6wbF4ep2Ny5&authKey=OknnnniiEOFaIZNhEl2dUIhSrKPB8wGrEYu1AGyS01Y6XXz7doQl7v%2FitvPRr3ii" style="display:flex;align-items:center;justify-content:center;">
  <img data-original="https://user-gold-cdn.xitu.io/2019/11/23/16e974e0f084ca30?w=540&h=740&f=jpeg&s=125975">
</a>

## 福利
有需要刷钻,刷会员,刷腾讯视频会员,涨粉丝等等相关意向的小伙伴们,可以访问作者推荐的超值代刷网站:[qq.usword.cn](http://qq.usword.cn), 也可以扫描下方二维码

![](https://user-gold-cdn.xitu.io/2019/11/23/16e974ec2da5a31f?w=300&h=390&f=png&s=12507)

## 最后
>如果老板觉得这篇文章有帮助,就大方的点个赞吧,老板大气,老板最帅~

If you have some questions after you see this article, you can contact me or you can find some info by clicking these links.
- [juejin@wsm's juejin](https://juejin.im/user/5d1079ab6fb9a07ed4410cc0)
- Gi[tHub@1046224544](https://github.com/1046224544)
- [Segmentfault@wsm](https://segmentfault.com/u/98kk)
查看原文

赞 4 收藏 2 评论 4

达摩师兄 收藏了文章 · 2019-11-11

vue + typescript 项目起手式

vue + typescript 新项目起手式

最后更新于2018-06-30,技术文具有时效性,请知悉

我知道你们早就想用上 vue + ts 强类型了

还有后续 vue + typescript 进阶篇

  • 安装vue-cli
  • 安装ts依赖
  • 配置 webpack
  • 添加 tsconfig.json
  • 添加 tslint.json
  • ts 识别 .vue
  • 改造 .vue文件

什么是typescript

TypeScriptJavaScript 的强类型版本。然后在编译期去掉类型和特有语法,生成纯粹的 JavaScript 代码。由于最终在浏览器中运行的仍然是 JavaScript,所以 TypeScript 并不依赖于浏览器的支持,也并不会带来兼容性问题。

TypeScriptJavaScript 的超集,这意味着他支持所有的 JavaScript 语法。并在此之上对 JavaScript 添加了一些扩展,如 class / interface / module 等。这样会大大提升代码的可阅读性。

与此同时,TypeScript 也是 JavaScript ES6 的超集,GoogleAngular 2.0 也宣布采用 TypeScript 进行开发。这更是充分说明了这是一门面向未来并且脚踏实地的语言。

强类型语言的优势在于静态类型检查,具体可以参见 http://www.zhihu.com/question... 的回答。概括来说主要包括以下几点:

  1. 静态类型检查
  2. IDE 智能提示
  3. 代码重构
  4. 可读性
静态类型检查可以避免很多不必要的错误, 不用在调试的时候才发现问题

前言

随着vue2.5 更好的 TypeScript 集成,同时因为新开项目的契机,故准备动手尝试一下typescript + vue

都说ts万般好,不如一个段子来的直观,一个程序员自从用上了ts之后,连续写了3000+行代码一次编译通过一气呵成,然后很激动的打电话跟老婆炫耀这件事情,老婆回了一句

之前看文章或者 demo 一直认为 vue + typescript 之后就不能友好的写.vue单文件,并且我也在各种 live 中问vue + ts 或者 flow的集成,也一直没有问出什么好的实践,但是本上强上ts的念头,一个字,就是干!

终于决定自己动手,那接下来从 vue-cli 开始配置 ts,看看事实上集成 ts 的体验到底是如何呢?


先贴一张最后配置完毕的.vue文件 ,template 和 style 跟以前的写法保持一致,只有 script 的变化

图片描述

起手vue-cli

这步应该不用写了

Vue 引入 TypeScript

首先Cli之后,接下来需要安装一些必要/以后需要的插件

安装vue的官方插件
npm i vue-class-component vue-property-decorator --save

// ts-loader typescript 必须安装,其他的相信你以后也会装上的
npm i ts-loader typescript tslint tslint-loader tslint-config-standard --save-dev

这些库大体的作用,可以按需引入:

配置 webpack

首先找到./build/webpack.base.conf.js

  • 找到entry.appmain.js 改成 main.ts, 顺便把项目文件中的main.js也改成main.ts, 里面内容保持不变
entry: {
  app: './src/main.ts'
}
  • 找到resolve.extensions 里面加上.ts 后缀 (是为了之后引入.ts的时候不写后缀)
  resolve: {
    extensions: ['.js', '.vue', '.json', '.ts'],
    alias: {
      '@': resolve('src')
    }
  }
  • 找到module.rules 添加webpack对.ts的解析
module: {
  rules: [
    {
      test: /\.(js|vue)$/,
      loader: 'eslint-loader',
      enforce: 'pre',
      include: [resolve('src'), resolve('test')],
      options: {
        formatter: require('eslint-friendly-formatter')
      }
    },
// 从这里复制下面的代码就可以了
    {
      test: /\.ts$/,
      exclude: /node_modules/,
      enforce: 'pre',
      loader: 'tslint-loader'
    },
    {
      test: /\.tsx?$/,
      loader: 'ts-loader',
      exclude: /node_modules/,
      options: {
        appendTsSuffixTo: [/\.vue$/],
      }
    },
// 复制以上的
  }
}

是不是加完了,那现在来解释一下

ts-loader 会检索当前目录下的 tsconfig.json 文件,根据里面定义的规则来解析.ts文件(就跟.babelrc的作用一样)

tslint-loader 作用等同于 eslint-loader

添加 tsconfig.json

接下来在根路径下创建tsconfig.json文件

这里有一份参考的 tsconfig.json 配置,完成的配置请点击 tsconfig.json

{
  // 编译选项
  "compilerOptions": {
    // 输出目录
    "outDir": "./output",
    // 是否包含可以用于 debug 的 sourceMap
    "sourceMap": true,
    // 以严格模式解析
    "strict": true,
    // 采用的模块系统
    "module": "esnext",
    // 如何处理模块
    "moduleResolution": "node",
    // 编译输出目标 ES 版本
    "target": "es5",
    // 允许从没有设置默认导出的模块中默认导入
    "allowSyntheticDefaultImports": true,
    // 将每个文件作为单独的模块
    "isolatedModules": false,
    // 启用装饰器
    "experimentalDecorators": true,
    // 启用设计类型元数据(用于反射)
    "emitDecoratorMetadata": true,
    // 在表达式和声明上有隐含的any类型时报错
    "noImplicitAny": false,
    // 不是函数的所有返回路径都有返回值时报错。
    "noImplicitReturns": true,
    // 从 tslib 导入外部帮助库: 比如__extends,__rest等
    "importHelpers": true,
    // 编译过程中打印文件名
    "listFiles": true,
    // 移除注释
    "removeComments": true,
    "suppressImplicitAnyIndexErrors": true,
    // 允许编译javascript文件
    "allowJs": true,
    // 解析非相对模块名的基准目录
    "baseUrl": "./",
    // 指定特殊模块的路径
    "paths": {
      "jquery": [
        "node_modules/jquery/dist/jquery"
      ]
    },
    // 编译过程中需要引入的库文件的列表
    "lib": [
      "dom",
      "es2015",
      "es2015.promise"
    ]
  }
}

顺便贴一份自己的配置

{
  "include": [
    "src/**/*"
  ],
  "exclude": [
    "node_modules"
  ],
  "compilerOptions": {
    "allowSyntheticDefaultImports": true,
    "experimentalDecorators": true,
    "allowJs": true,
    "module": "esnext",
    "target": "es5",
    "moduleResolution": "node",
    "isolatedModules": true,
    "lib": [
      "dom",
      "es5",
      "es2015.promise"
    ],
    "sourceMap": true,
    "pretty": true
  }
}

添加 tslint.json

在根路径下创建tslint.json文件

这里就很简单了,就是 引入 tsstandard 规范

{
  "extends": "tslint-config-standard",
  "globals": {
    "require": true
  }
}

让 ts 识别 .vue

由于 TypeScript 默认并不支持 *.vue 后缀的文件,所以在 vue 项目中引入的时候需要创建一个 vue-shim.d.ts 文件,放在项目项目对应使用目录下,例如 src/vue-shim.d.ts

declare module "*.vue" {
  import Vue from "vue";
  export default Vue;
}
敲黑板,下面有重点!

意思是告诉 TypeScript*.vue 后缀的文件可以交给 vue 模块来处理。

而在代码中导入 *.vue 文件的时候,需要写上 .vue 后缀。原因还是因为 TypeScript 默认只识别 *.ts 文件,不识别 *.vue 文件:

import Component from 'components/component.vue'

改造 .vue 文件

在这之前先让我们了解一下所需要的插件(下面的内容需要掌握es7装饰器, 就是下面使用的@符号)

vue-class-component

vue-class-componentVue 组件进行了一层封装,让 Vue 组件语法在结合了 TypeScript 语法之后更加扁平化:

<template>
  <div>
    <input v-model="msg">
    <p>msg: {{ msg }}</p>
    <p>computed msg: {{ computedMsg }}</p>
    <button @click="greet">Greet</button>
  </div>
</template>

<script lang="ts">
  import Vue from 'vue'
  import Component from 'vue-class-component'

  @Component
  export default class App extends Vue {
    // 初始化数据
    msg = 123

    // 声明周期钩子
    mounted () {
      this.greet()
    }

    // 计算属性
    get computedMsg () {
      return 'computed ' + this.msg
    }

    // 方法
    greet () {
      alert('greeting: ' + this.msg)
    }
  }
</script>

上面的代码跟下面的代码作用是一样的

export default {
  data () {
    return {
      msg: 123
    }
  }

  // 声明周期钩子
  mounted () {
    this.greet()
  }

  // 计算属性
  computed: {
    computedMsg () {
      return 'computed ' + this.msg
    }
  }

  // 方法
  methods: {
    greet () {
      alert('greeting: ' + this.msg)
    }
  }
}

vue-property-decorator

vue-property-decorator 是在 vue-class-component 上增强了更多的结合 Vue 特性的装饰器,新增了这 7 个装饰器:

  • @Emit
  • @Inject
  • @Model
  • @Prop
  • @Provide
  • @Watch
  • @Component (从 vue-class-component 继承)

在这里列举几个常用的@Prop/@Watch/@Component, 更多信息,详见官方文档

import { Component, Emit, Inject, Model, Prop, Provide, Vue, Watch } from 'vue-property-decorator'

@Component
export class MyComponent extends Vue {
  
  @Prop()
  propA: number = 1

  @Prop({ default: 'default value' })
  propB: string

  @Prop([String, Boolean])
  propC: string | boolean

  @Prop({ type: null })
  propD: any

  @Watch('child')
  onChildChanged(val: string, oldVal: string) { }
}

上面的代码相当于:

export default {
  props: {
    checked: Boolean,
    propA: Number,
    propB: {
      type: String,
      default: 'default value'
    },
    propC: [String, Boolean],
    propD: { type: null }
  }
  methods: {
    onChildChanged(val, oldVal) { }
  },
  watch: {
    'child': {
      handler: 'onChildChanged',
      immediate: false,
      deep: false
    }
  }
}

开始修改App.vue文件

  1. script 标签上加上 lang="ts", 意思是让webpack将这段代码识别为typescript 而非javascript
  2. 修改vue组件的构造方式( 跟react组件写法有点类似, 详见官方 ), 如下图
  3. vue-property-decorator语法改造之前代码

clipboard.png

当然也可以直接复制下面的代码替换就可以了

<template>
  <div id="app">
    <img data-original="./assets/logo.png">
    <router-view/>
  </div>
</template>

<script lang="ts">
import Vue from 'vue'
import Component from 'vue-class-component'

@Component({})
export default class App extends Vue {
}
</script>

<style>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}

</style>

接下来用相同的方式修改HelloWorld.vue即可

npm run dev

这个时候运行项目就应该能正常跑起来了

到这里我们的配置就已经结束了

最后

如果按照文章没有配置出来,可以参考此repovue-typescript-starter (安全按照文章一步一步操作的版本)

总的来说,就如本文最初讲,ts 从数据类型、结构入手,通过静态类型检测来增强你代码的健壮性,从而避免 bug 的产生。

同时可以继续使用.vue单文件

而且我个人认为加上了typescript,项目逼格提升2个level,也能让后端大哥们不吐槽js弱语言的诟病了

相信之后 vue 对于 ts 的集成会更加友善,期待尤大之后的动作

还有后续 vue + typescript 进阶篇

参考链接/推荐阅读

查看原文

达摩师兄 赞了文章 · 2019-11-11

vue + typescript 项目起手式

vue + typescript 新项目起手式

最后更新于2018-06-30,技术文具有时效性,请知悉

我知道你们早就想用上 vue + ts 强类型了

还有后续 vue + typescript 进阶篇

  • 安装vue-cli
  • 安装ts依赖
  • 配置 webpack
  • 添加 tsconfig.json
  • 添加 tslint.json
  • ts 识别 .vue
  • 改造 .vue文件

什么是typescript

TypeScriptJavaScript 的强类型版本。然后在编译期去掉类型和特有语法,生成纯粹的 JavaScript 代码。由于最终在浏览器中运行的仍然是 JavaScript,所以 TypeScript 并不依赖于浏览器的支持,也并不会带来兼容性问题。

TypeScriptJavaScript 的超集,这意味着他支持所有的 JavaScript 语法。并在此之上对 JavaScript 添加了一些扩展,如 class / interface / module 等。这样会大大提升代码的可阅读性。

与此同时,TypeScript 也是 JavaScript ES6 的超集,GoogleAngular 2.0 也宣布采用 TypeScript 进行开发。这更是充分说明了这是一门面向未来并且脚踏实地的语言。

强类型语言的优势在于静态类型检查,具体可以参见 http://www.zhihu.com/question... 的回答。概括来说主要包括以下几点:

  1. 静态类型检查
  2. IDE 智能提示
  3. 代码重构
  4. 可读性
静态类型检查可以避免很多不必要的错误, 不用在调试的时候才发现问题

前言

随着vue2.5 更好的 TypeScript 集成,同时因为新开项目的契机,故准备动手尝试一下typescript + vue

都说ts万般好,不如一个段子来的直观,一个程序员自从用上了ts之后,连续写了3000+行代码一次编译通过一气呵成,然后很激动的打电话跟老婆炫耀这件事情,老婆回了一句

之前看文章或者 demo 一直认为 vue + typescript 之后就不能友好的写.vue单文件,并且我也在各种 live 中问vue + ts 或者 flow的集成,也一直没有问出什么好的实践,但是本上强上ts的念头,一个字,就是干!

终于决定自己动手,那接下来从 vue-cli 开始配置 ts,看看事实上集成 ts 的体验到底是如何呢?


先贴一张最后配置完毕的.vue文件 ,template 和 style 跟以前的写法保持一致,只有 script 的变化

图片描述

起手vue-cli

这步应该不用写了

Vue 引入 TypeScript

首先Cli之后,接下来需要安装一些必要/以后需要的插件

安装vue的官方插件
npm i vue-class-component vue-property-decorator --save

// ts-loader typescript 必须安装,其他的相信你以后也会装上的
npm i ts-loader typescript tslint tslint-loader tslint-config-standard --save-dev

这些库大体的作用,可以按需引入:

配置 webpack

首先找到./build/webpack.base.conf.js

  • 找到entry.appmain.js 改成 main.ts, 顺便把项目文件中的main.js也改成main.ts, 里面内容保持不变
entry: {
  app: './src/main.ts'
}
  • 找到resolve.extensions 里面加上.ts 后缀 (是为了之后引入.ts的时候不写后缀)
  resolve: {
    extensions: ['.js', '.vue', '.json', '.ts'],
    alias: {
      '@': resolve('src')
    }
  }
  • 找到module.rules 添加webpack对.ts的解析
module: {
  rules: [
    {
      test: /\.(js|vue)$/,
      loader: 'eslint-loader',
      enforce: 'pre',
      include: [resolve('src'), resolve('test')],
      options: {
        formatter: require('eslint-friendly-formatter')
      }
    },
// 从这里复制下面的代码就可以了
    {
      test: /\.ts$/,
      exclude: /node_modules/,
      enforce: 'pre',
      loader: 'tslint-loader'
    },
    {
      test: /\.tsx?$/,
      loader: 'ts-loader',
      exclude: /node_modules/,
      options: {
        appendTsSuffixTo: [/\.vue$/],
      }
    },
// 复制以上的
  }
}

是不是加完了,那现在来解释一下

ts-loader 会检索当前目录下的 tsconfig.json 文件,根据里面定义的规则来解析.ts文件(就跟.babelrc的作用一样)

tslint-loader 作用等同于 eslint-loader

添加 tsconfig.json

接下来在根路径下创建tsconfig.json文件

这里有一份参考的 tsconfig.json 配置,完成的配置请点击 tsconfig.json

{
  // 编译选项
  "compilerOptions": {
    // 输出目录
    "outDir": "./output",
    // 是否包含可以用于 debug 的 sourceMap
    "sourceMap": true,
    // 以严格模式解析
    "strict": true,
    // 采用的模块系统
    "module": "esnext",
    // 如何处理模块
    "moduleResolution": "node",
    // 编译输出目标 ES 版本
    "target": "es5",
    // 允许从没有设置默认导出的模块中默认导入
    "allowSyntheticDefaultImports": true,
    // 将每个文件作为单独的模块
    "isolatedModules": false,
    // 启用装饰器
    "experimentalDecorators": true,
    // 启用设计类型元数据(用于反射)
    "emitDecoratorMetadata": true,
    // 在表达式和声明上有隐含的any类型时报错
    "noImplicitAny": false,
    // 不是函数的所有返回路径都有返回值时报错。
    "noImplicitReturns": true,
    // 从 tslib 导入外部帮助库: 比如__extends,__rest等
    "importHelpers": true,
    // 编译过程中打印文件名
    "listFiles": true,
    // 移除注释
    "removeComments": true,
    "suppressImplicitAnyIndexErrors": true,
    // 允许编译javascript文件
    "allowJs": true,
    // 解析非相对模块名的基准目录
    "baseUrl": "./",
    // 指定特殊模块的路径
    "paths": {
      "jquery": [
        "node_modules/jquery/dist/jquery"
      ]
    },
    // 编译过程中需要引入的库文件的列表
    "lib": [
      "dom",
      "es2015",
      "es2015.promise"
    ]
  }
}

顺便贴一份自己的配置

{
  "include": [
    "src/**/*"
  ],
  "exclude": [
    "node_modules"
  ],
  "compilerOptions": {
    "allowSyntheticDefaultImports": true,
    "experimentalDecorators": true,
    "allowJs": true,
    "module": "esnext",
    "target": "es5",
    "moduleResolution": "node",
    "isolatedModules": true,
    "lib": [
      "dom",
      "es5",
      "es2015.promise"
    ],
    "sourceMap": true,
    "pretty": true
  }
}

添加 tslint.json

在根路径下创建tslint.json文件

这里就很简单了,就是 引入 tsstandard 规范

{
  "extends": "tslint-config-standard",
  "globals": {
    "require": true
  }
}

让 ts 识别 .vue

由于 TypeScript 默认并不支持 *.vue 后缀的文件,所以在 vue 项目中引入的时候需要创建一个 vue-shim.d.ts 文件,放在项目项目对应使用目录下,例如 src/vue-shim.d.ts

declare module "*.vue" {
  import Vue from "vue";
  export default Vue;
}
敲黑板,下面有重点!

意思是告诉 TypeScript*.vue 后缀的文件可以交给 vue 模块来处理。

而在代码中导入 *.vue 文件的时候,需要写上 .vue 后缀。原因还是因为 TypeScript 默认只识别 *.ts 文件,不识别 *.vue 文件:

import Component from 'components/component.vue'

改造 .vue 文件

在这之前先让我们了解一下所需要的插件(下面的内容需要掌握es7装饰器, 就是下面使用的@符号)

vue-class-component

vue-class-componentVue 组件进行了一层封装,让 Vue 组件语法在结合了 TypeScript 语法之后更加扁平化:

<template>
  <div>
    <input v-model="msg">
    <p>msg: {{ msg }}</p>
    <p>computed msg: {{ computedMsg }}</p>
    <button @click="greet">Greet</button>
  </div>
</template>

<script lang="ts">
  import Vue from 'vue'
  import Component from 'vue-class-component'

  @Component
  export default class App extends Vue {
    // 初始化数据
    msg = 123

    // 声明周期钩子
    mounted () {
      this.greet()
    }

    // 计算属性
    get computedMsg () {
      return 'computed ' + this.msg
    }

    // 方法
    greet () {
      alert('greeting: ' + this.msg)
    }
  }
</script>

上面的代码跟下面的代码作用是一样的

export default {
  data () {
    return {
      msg: 123
    }
  }

  // 声明周期钩子
  mounted () {
    this.greet()
  }

  // 计算属性
  computed: {
    computedMsg () {
      return 'computed ' + this.msg
    }
  }

  // 方法
  methods: {
    greet () {
      alert('greeting: ' + this.msg)
    }
  }
}

vue-property-decorator

vue-property-decorator 是在 vue-class-component 上增强了更多的结合 Vue 特性的装饰器,新增了这 7 个装饰器:

  • @Emit
  • @Inject
  • @Model
  • @Prop
  • @Provide
  • @Watch
  • @Component (从 vue-class-component 继承)

在这里列举几个常用的@Prop/@Watch/@Component, 更多信息,详见官方文档

import { Component, Emit, Inject, Model, Prop, Provide, Vue, Watch } from 'vue-property-decorator'

@Component
export class MyComponent extends Vue {
  
  @Prop()
  propA: number = 1

  @Prop({ default: 'default value' })
  propB: string

  @Prop([String, Boolean])
  propC: string | boolean

  @Prop({ type: null })
  propD: any

  @Watch('child')
  onChildChanged(val: string, oldVal: string) { }
}

上面的代码相当于:

export default {
  props: {
    checked: Boolean,
    propA: Number,
    propB: {
      type: String,
      default: 'default value'
    },
    propC: [String, Boolean],
    propD: { type: null }
  }
  methods: {
    onChildChanged(val, oldVal) { }
  },
  watch: {
    'child': {
      handler: 'onChildChanged',
      immediate: false,
      deep: false
    }
  }
}

开始修改App.vue文件

  1. script 标签上加上 lang="ts", 意思是让webpack将这段代码识别为typescript 而非javascript
  2. 修改vue组件的构造方式( 跟react组件写法有点类似, 详见官方 ), 如下图
  3. vue-property-decorator语法改造之前代码

clipboard.png

当然也可以直接复制下面的代码替换就可以了

<template>
  <div id="app">
    <img data-original="./assets/logo.png">
    <router-view/>
  </div>
</template>

<script lang="ts">
import Vue from 'vue'
import Component from 'vue-class-component'

@Component({})
export default class App extends Vue {
}
</script>

<style>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}

</style>

接下来用相同的方式修改HelloWorld.vue即可

npm run dev

这个时候运行项目就应该能正常跑起来了

到这里我们的配置就已经结束了

最后

如果按照文章没有配置出来,可以参考此repovue-typescript-starter (安全按照文章一步一步操作的版本)

总的来说,就如本文最初讲,ts 从数据类型、结构入手,通过静态类型检测来增强你代码的健壮性,从而避免 bug 的产生。

同时可以继续使用.vue单文件

而且我个人认为加上了typescript,项目逼格提升2个level,也能让后端大哥们不吐槽js弱语言的诟病了

相信之后 vue 对于 ts 的集成会更加友善,期待尤大之后的动作

还有后续 vue + typescript 进阶篇

参考链接/推荐阅读

查看原文

赞 396 收藏 378 评论 103

达摩师兄 发布了文章 · 2019-10-21

7天撸完KTV点歌系统,含后台管理系统(完整版)

最近手有点痒琢磨着做个啥,朝思暮想还是写个KTV点歌系统,模拟了一下KTV开户的思路,7天累死我了,不过技术点还挺多的,希望你可以看完(〜^㉨^)〜

用Node(Express)教你写KTV点歌系统,包括前台内容和后台管理系统,整合Express框架和Mongodb数据库服务器开发;教你用Vue.JS,ElementUI和iViewUI写出超漂亮的页面,随心点歌随心听

思维导图

技术栈

  • 后端: Express + Mongodb + jsonwebtoken等等
  • 前端: Vue.JS + ElementUI + iViewUI + Axios等等

功能介绍

本项目分前台开发,后台开发和服务器开发

  • 用户听歌需要登录(路由守卫)
  • 用户需要到管理员申请账号和密码
  • 用户登录听歌(风格点歌,语种点歌,明星点歌,热门歌曲等等...)
  • 剩余时长30分钟提醒,到时间自动下机
  • 管理员对歌曲的增删改查
  • 管理员给用户开户,可以选择上机的时间
  • 管理员查看订单,删除订单,搜索订单
  • 管理员收藏歌曲,推荐到ktv推荐歌曲
  • 等等...

项目设计结构

-- 服务器基本架构
ktv-select_music-system
├── README.md
├── index.js  -- 后台文件入口
├── test.http  -- 测试文件
├── api  -- 路由文件
│    ├── admin.js  -- 配置管理员的操作
|    ├── music.js  -- 配置歌曲信息
|    ├── user.js  -- 配置用户的相关操作
|    └── safecode.js  -- 配置安全码
├── config -- 配置
|    ├── Date.js  -- 配置日期格式化插件
|    ├── delNoUse.js  -- 封装闲置删除闲置资源方法
|    ├── http.js  -- 配置跨域
|    ├── isBadAccount.js  -- 封装账户是否合法
|    ├── newaccount.js  -- 封装随机开户方法
|    ├── passport.js  -- 验证token是否合法
|    ├── uploadImg.js  -- 封装上传图片方法
|    └── uploadMusic.js  -- 封装上传歌曲方法
├── ktv-admin  --后台管理系统界面
├── ktv-client  --前台用户点歌项目界面
├── dbModel
|    └── **  -- Mongodb数据库的一些模型
├── mongodb
|    └── mongodb.js  -- 配置Mongodb,链接数据库 
├── secret
|    ├── mongodbURI.js  -- Mongodb地址
|    └── jwtkey.js  -- token的私钥
├── static -- 资源存放处
|    ├── music  -- 歌曲上传目标文件夹 
|    ├── poster  -- 歌曲海报上传目标文件夹
└──  └── view  -- 配置404文件
-- 后台管理系统架构
ktv-admin
├── README.md
├── public 
|    ├── index.html  -- vue挂载页面
|    └── **  -- 你可以在这里链接少量静态资源
├── src  -- 开发文件夹
|    ├── App.vue  -- Vue挂载根页面
|    ├── main.js  -- Vue程序入口文件,挂载各种组件
|    ├── router.js  -- Vue路由配置文件
|    ├── store.js  -- Vuex的状态管理文件
|    ├── assets  -- 静态资源文件夹
|    ├── components  --公共组件
|    |      └── nav.vue  -- 后台导航栏
|    ├── plugins  --插件
|    |      ├── axios.js   -- 配置跨域,拦截器等等 
|    |      ├── Date.js   -- 格式化日期 
|    |      └── wsmLoading.js   -- 加载动画Loading
|    ├── stores  -- 状态管理文件夹
|    |      └── adminStore.js  -- 管理员状态 
|    ├── views  -- 页面文件夹
|    |      ├── 404.vue   -- 404页面
|    |      ├── adminlikes.vue   -- 管理员处理ktv收藏歌曲
|    |      ├── allorders.vue   -- 订单管理
|    |      ├── Home.vue   -- 后台根页面
|    |      ├── Index.vue   -- 后台首页
|    |      ├── managemusic.vue   -- 音乐管理
|    |      ├── user_service.vue   -- 给用户开户
|    |      └── login.vue   -- 后台登录
└── babel.config.js  -- babel配置

-- 前台用户听歌架构
ktv-client
├── README.md
├── public 
|    ├── index.html  -- vue挂载页面
|    └── **  -- 你可以在这里链接少量静态资源
├── src  -- 开发文件夹
|    ├── App.vue  -- Vue挂载根页面
|    ├── main.js  -- Vue程序入口文件,挂载各种组件
|    ├── router.js  -- Vue路由配置文件
|    ├── store.js  -- Vuex的状态管理文件
|    ├── assets  -- 静态资源文件夹
|    ├── components  --公共组件
|    |      ├── bottomNav.vue  -- 底部音乐控制区域
|    |      └── topNav.vue  -- 顶部信息区域
|    ├── config  --配置
|    |      ├── addSong.js    --封装选取歌曲方法
|    |      ├── isBadAccount.js    --验证账户合法性
|    |      ├── isLogin.js    --是否登录
|    |      ├── nextSong.js    --封装下一首歌曲方法
|    |      └── prevSong.js    --封装上一首歌曲方法
|    ├── plugins  --插件
|    |      ├── axios.js   -- 配置跨域,拦截器等等 
|    |      └── wsmLoading.js   -- 加载动画Loading
|    ├── stores  -- 状态管理文件夹
|    |      └── song.js  -- 存储歌曲信息 
|    ├── views  -- 页面文件夹
|    |      ├── 404.vue   -- 404页面
|    |      ├── abc.vue   -- 拼音点歌
|    |      ├── artist.vue   -- 明星点歌
|    |      ├── Home.vue   -- 后台根页面
|    |      ├── Index.vue   -- 后台首页
|    |      ├── hot.vue   -- 热播歌曲
|    |      ├── ktvlikes.vue   -- ktv推荐歌曲
|    |      ├── selected.vue   -- 已选歌曲
|    |      ├── style.vue   -- 风格点歌
|    |      └── language.vue   -- 语种点歌
├── babel.config.js  -- babel配置
└── vue.config.js  -- vue配置

项目启动介绍

  1. 首先不要改变服务器端口,否则报错.
  2. 你需要在装有Node和Vue的环境中测试,如果其中一个没有请先下载(Node下载,Vue下载).
  3. 首先在最外层文件夹下载依赖:npm install 下载后端依赖,
  4. 接着进入ktv-client, npm install 下载用户前端依赖.
  5. 接着进入ktv-admin, npm install 下载管理员前端依赖.
  6. 以上工作完成后,使用命令npm run server 或者 node index &命令启动Node服务器,启动成功会显示:

    Server is running on port [8633].

    Mongodb is Connected.Please have a great coding.

  7. 进入ktv-client,打开命令板,使用命令npm run client启动前台用户项目,启动成功后用浏览器访问http://localhost:xxxx
  8. 进入ktv-admin,打开命令板,使用命令npm run admin启动后台管理系统项目,启动成功后用浏览器访问http://localhost:xxxx
  9. 本例中将Mongodb部署在本地电脑上,如果你仔细阅读了这篇文档,启动项目应该是很容易的。如果你把Mongodb部署在其他地方,请自行修改secret/mongodbURI.js配置文件信息。

10.项目启动成功

技术攻关

Date方法

由于脑袋不好使的原因加上js没有元素格式化日期的方法,就瞎掰一个(值得学习)

Date.js


/** 
*
*  @author: Mr_Wei 
*  @version: 1.0.0 
*  @description: 格式化日期
*  @Date: 2019/10/16 09:32
*
*/ 

Date.prototype.format = function(format) {
    var o = {
        "M+": this.getMonth() + 1, //月份
        "d+": this.getDate(), //日
        "H+": this.getHours(), //小时
        "m+": this.getMinutes(), //分
        "s+": this.getSeconds(), //秒
        "q+": Math.floor((this.getMonth() + 3) / 3), //季度
        "f+": this.getMilliseconds() //毫秒
    };
    if (/(y+)/.test(format))
        format = format.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length));
    for (var k in o)
        if (new RegExp("(" + k + ")").test(format))
            format = format.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
    return format;
}

export default Date.prototype.format


然后我们使用其格式日期
require(Date);
// const now = new Date().format("yyyy/MM/dd HH:mm:ss.S");
const now = new Date().format("yyyy/MM/dd HH:mm:ss");

验证码(svg-captcha)

svg-captcha验证码的运用,防止暴力破解密码,加强安全性.
详细的文档地址:svg-captcha

使用验证码

// 后台生成验证码
router.get("/getCaptcha", (req, res) => {
    var captcha = svgCaptcha.create({  
        // 翻转颜色  
        inverse: false,  
        // 字体大小  
        fontSize: 38,  
        // 噪声线条数  
        noise: 3,  
        // 宽度  
        width: 80,  
        // 高度  
        height: 32,  
      });  
      // 保存到session,忽略大小写  
      req.session = captcha.text.toLowerCase(); 
      console.log(req.session); //0xtg 生成的验证码
      //保存到cookie 方便前端调用验证
      res.cookie('captcha', req.session); 
      res.setHeader('Content-Type', 'image/svg+xml');
      res.send(String(captcha.data));
      res.end();
})


// 前台获取验证码
--HTML
<img width="80" style="background:#EEE9E9;margin-left:30px;" ref="captcha" height="32" data-original="http://localhost:3001/api/user/getCaptcha" @click="refreshCaptcha">

--js
// 获取验证码cookie
getCookie(cname){
    var name = cname + "=";
    var ca = document.cookie.split(';');
    for(var i=0; i<ca.length; i++){
        var c = ca[i].trim();
        if (c.indexOf(name)==0) return c.substring(name.length,c.length);
    }
    return "";
},
// 刷新验证码
refreshCaptcha(){
    this.$refs.captcha.src = "http://localhost:3001/api/user/getCaptcha?d=" + Math.random();
},

最后用 填写的验证码进行对比

上传歌曲或图片

formidable来处理文件上传信息,用起来方便,很友好,如果你没有接触过文件操作,赶紧收藏起来

封装歌曲方法uploadMusic.js

/** 
*
*  @author: Mr_Wei 
*  @version: 1.0.0 
*  @description: 封装上传音乐方法
*  @Date: 2019/10/16 08:35
*
*/ 

const fs = require('fs');
const path = require('path');
const formidable = require('formidable');  // 文件处理库
const formatTime = require('silly-datetime');  // 格式化数据

module.exports = (req, res) => {
    
    let form = new formidable.IncomingForm();  //创建上传表单
    form.encoding = 'utf-8';  // 设置编码格式
    form.uploadDir = path.join(__dirname, '../static/music'); // 设置上传目录(这个目录必须先创建好)
    form.keepExtensions = true;  // 保留文件后缀名
    form.maxFieldsSize = 20 * 1024 *1024; // 设置文件大小

    /* 格式化form数据 */
    form.parse(req, (err, fields, files) => {
        let file = files.file;
        /* 获取异常 */
        if(err) {
            return res.status(500).json({'status': 500, result: '服务器内部错误'});
        }
        if(file.size > form.maxFieldsSize) {
            fs.unlink(file.path);
            return res.status(412).json({'status': 412, result: '音频不能超过20M'});
        }

        /* 存储后缀名 */
        let extName = '';
        switch (file.type) {
            case 'audio/mp3':
                extName = 'mp3';
                break;
        }
        if(extName.length == 0) {
            fs.unlink(file.path);
            return res.status(412).json({'status': 412, result: '只支持mp3格式音频'});
        }
        /* 拼接新的文件名 */
        let time = formatTime.format(new Date(), 'YYYYMMDDHHmmss');
        let num = Math.floor(Math.random() * 8999 + 10000);
        let songName = `${time}_${num}.${extName}`;
        let newPath = form.uploadDir + '/' + songName;

        /* 更改名字和路径 */
        fs.rename(file.path, newPath, (err) => {
            if(err) {
                return res.status(500).json({'status': 500, result: '音频上传失败'});
            } else {
                return res.send({'status': 200, 'msg': '音频上传成功', result: {src: songName}});
            }
        })
        
    })
};

Vue、ElementUI分页使用

关于ElementUI分页详细请见:ElementUI的Pagination分页学习

上图

-- html
<el-pagination
    v-if='paginations.total > 0'
    :page-sizes="paginations.page_sizes"
    :page-size="paginations.page_size"
    :layout="paginations.layout"
    :total="paginations.total"
    :current-page.sync='paginations.page_index'
    @current-change='handleCurrentChange'
    @size-change='handleSizeChange'>
</el-pagination>

-- js
data(){
    return{
        allUsers:[],  // 用来存储最终信息, 被显示的dom点调用
        allTableData:[],  // 用户承接分页设置的数据
        paginations: {   // 分页组件信息
            page_index: 1, // 当前位于哪页
            total: 0, // 总数
            page_size: 5, // 1页显示多少条
            page_sizes: [5, 10, 15, 20], //每页显示多少条
            layout: "total, sizes, prev, pager, next, jumper" // 翻页属性
        },
    }
},
methods:{
    // 获取当前页
    handleCurrentChange(page) {
        let sortnum = this.paginations.page_size * (page - 1);
        let table = this.allTableData.filter((item, index) => {
            return index >= sortnum;
        });
        // 设置默认分页数据
        this.getAllUsers = table.filter((item, index) => {
            return index < this.paginations.page_size;
        });
        this.getAllUsers = table.filter((item, index) => {
            return index < this.paginations.page_size;
        });
    },
    // 切换size
    handleSizeChange(page_size) {
        this.paginations.page_index = 1;
        this.paginations.page_size = page_size;
        this.getAllUsers = this.allTableData.filter((item, index) => {
            return index < page_size;
        });
    },
     // 总页数
    setPaginations() {
        this.paginations.total = this.allTableData.length;
        this.paginations.page_index = 1;
        this.paginations.page_size = 5;
        // 设置默认分页数据
        this.getAllUsers = this.allTableData.filter((item, index) => {
            return index < this.paginations.page_size;
        });
    },
}

没了吗?对,分页就是这么简单!你学会了吗?有些前端开发的同学总是对分页比较陌生,学会这个,让你不再产生烦恼!

token和自定义验证合法性

jsonwebtoken是对用户信息加密成不可逆向破解的token.关于passport-jwt,是用来对用户请求时所带的token信息进行过期验证,如果超过签证的合法时间,则会请前台发出token失效的信息,提示用户重新获取合法的token信息,否则无法继续请求加密的信息;

用法

- passport-jwt
const key = require("../config/keys").KEYORSECRET;
const JwtStrategy = require('passport-jwt').Strategy,
      ExtractJwt = require('passport-jwt').ExtractJwt;
var opts = {}
opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken();
opts.secretOrKey = key;

module.exports = passport => {
    passport.use(new JwtStrategy(opts, (jwt_payload, done) => {
        UserInfo.findById(jwt_payload.id)
                .then(user => {
                    if (user) {
                        return done(null, user);
                    } else {
                        return done(null, false);
                        // or you could create a new account
                    }
                })
    }));
}



// 设置token
// 规则
 const rule = {
    id:String(userinfo._id),
    username:userinfo.username,
    email:userinfo.email,
    date:user.date,
    signdate:userinfo.signdate,
    signcount:userinfo.signcount,
    avatar:userinfo.avatar,
    phone:userinfo.phone
};

// 签证加密
// jwt.sign(规则, key(私钥), {配置:比如过期时长}, (err, token){ 响应程序 })
jwt.sign(rule,key,{expiresIn:7200},(err, token) => {
    if(err) throw err;
    res.json({"token" : "Bearer " + token})
})



自定义验证方法
/** 
*
*  @author: Mr_Wei 
*  @version: 1.0.0 
*  @description: 判断是否过期用户
*  @Date: 2019/10/19 12:19
*
*/ 
const UserOrOrders = require("../dbModel/user");
module.exports = async params => {
    
    
    const flag = await new Promise((resolve) => {
        if(params){
            const account = params.account;
            UserOrOrders.findOne({account})
                .then(user => {
                    if(user){
                        if(new Date().getTime() > new Date(user.endTime).getTime()){
                            console.log("过期用户");
                            // 处理
                            return resolve(false);
                        }else{
                            console.log("合法用户");
                            return resolve(true);
                        }
                    }else{
                        return resolve(false);
                    }
                })
        }else{
            console.log("不合法用户");
            return resolve(false);
        }
        
    }) 
    return flag;
}




使用:
// 测试  isBadAccount(params)方法
router.post("/test", passport.authenticate("jwt", {session:false}), async (req, res) => {
    // console.log(req.user)
    if(await isBadAccount(req.user)){
        // do something
        res.send("OK");
    }else{
        res.status(401).json({status:"401", result:"帐号过期,请联系管理员"})
    }
})

详细的文档地址:Passport-Jwt合法验证,token加密

截图

后台管理系统

前台点歌界面

源码在这里

以上代码均已上传 github

https://github.com/1046224544/ktv-select_music-system

联系

如果大家有兴趣,欢迎star. 欢迎大家加入我的前端交流群:866068198 ,一起交流学习前端技术。博主目前一直在自学Node中,技术有限,如果可以,会尽力给大家提供一些帮助,或是一些学习方法.

  • 群二维码

其他

最后

If you have some questions after you see this article, you can contact me or you can find some info by clicking these links.

如果对你有帮助,请赏个star~github地址
查看原文

赞 1 收藏 0 评论 0

达摩师兄 发布了文章 · 2019-10-13

用Node+VueCli+ElemenetUI+MongoDB教你开发综合性教程网站,包括后台管理系统

运用Node手摸手教你写一个教程网站(遵循响应式),包括前台内容和后台管理系统,整合Express框架和Mongodb数据库服务器开发;教你用Vue.JS,ElementUI和iViewUI写出超漂亮的页面

  • 本项目是作者原创, 转载前请留言或联系作者!!!
如果对你有帮助,请赏个star,谢谢哦~github地址

技术栈

  • 后端: Express + Mongodb + passport-jwt + jsonwebtoken等等
  • 前端: Vue.JS + ElementUI + iViewUI + Axios等等

测试环境

VSCode + Node8.9.4 + Google(75.xxx) + VueCli(3.7.0)

功能介绍

  • 学习网站上的知识
  • 搜索功能
  • 留言功能
  • 查看视频功能
  • 欣赏歌曲
  • 文章管理
  • 每日签到
  • 购买课程订单管理
  • 后台管理
  • 等等...

项目设计

-- 后台基本架构
Node-Vue-App
├── README.md
├── server.js  -- 后台文件入口
├── test.http  -- 测试文件
├── api  -- 路由文件
│    ├── admin.js  -- 配置管理员的操作
│    ├── article.js  -- 配置文章的相关操作
|    ├── index.js  -- 配置首页的相关数据
|    ├── messagewall.js  -- 配置留言社区
|    ├── music.js  -- 配置歌曲信息
|    ├── user.js  -- 记录用户的信息,配置用户的相关操作
|    └── vipcourse.js  -- 配置VIP课程的相关操作
├── config -- 配置文件
|    ├── Date.js  -- 配置日期格式化插件
|    ├── http.js  -- 配置跨域
|    ├── keys.js  -- 配置token的签证密钥
|    └── MongodbURI.js  -- 配置Mongodb一些信息
├── model
|    └── **  -- 自定义数据库的Mongodb模型
├── mongodb
|    └── mongodb.js  -- 配置Mongodb,链接数据库 
├── passport
|    └── passport.js  -- 验证token的合法性
├── static
|    └── **  -- 静态文件存放处 
├── client
|    └── **  -- (Vue)前台页面构架
-- 前台页面架构
Client
├── README.md
├── public
|    ├── index.html  -- vue挂载页面
|    └── **  -- 你可以在这里链接少量静态资源
├── src  -- 开发文件夹
|    ├── App.vue  -- Vue挂载根页面
|    ├── http.js  -- 配置http,响应拦截操作
|    ├── main.js  -- Vue程序入口文件,挂载各种组件
|    ├── router.js  -- Vue路由配置文件
|    ├── store.js  -- Vuex的状态管理文件
|    ├── assets
|    |     └── **  -- 存放静态资源
|    ├── components
|    |     └── pcnav.vue  -- 导航栏
|    ├── myplugin  -- 配置插件
|    |     ├── Date.js  -- 格式化日期
|    |     └── Loading.js  -- 加载动画
|    ├── store  -- 状态储存
|    |     ├── adminstore.js  -- 管理员状态
|    |     ├── musicstore.js  -- 歌曲信息
|    |     ├── searchstore.js  -- 搜索信息
|    |     └── userstore.js  -- 用户信息
|    ├── views  -- 页面文件
|    |     ├── search.vue  -- 搜索页面
|    |     ├── NotFound.vue  -- 404页面
|    |     ├── Home.vue  -- 主页面
|    |     ├── Index.vue  -- 前端首页
|    |     ├── adminPage  -- 管理员
|    |     |     ├── adminindex.vue  -- 后台首页
|    |     |     ├── adminlogin.vue  -- 后台登录页面
|    |     |     ├── adminnav.vue  -- 后台导航栏
|    |     |     ├── index.vue  -- 后台根挂载点
|    |     |     ├── mainarea.vue  -- 后台页面主区域挂载点
|    |     |     └── components  -- 后台组件
|    |     |     |     ├── addarticle.vue  -- 添加文章
|    |     |     |     ├── buycourselogs.vue  -- 购买课程记录
|    |     |     |     ├── communitymessage.vue  -- 社区留言
|    |     |     |     ├── cssmessage.vue  -- VIP课程css留言
|    |     |     |     ├── editcss.vue  -- 编辑CSS文章
|    |     |     |     ├── edithtml.vue  -- 编辑HTML文章
|    |     |     |     ├── editjavascript.vue  -- 编辑Javascript文章
|    |     |     |     ├── homepagedata.vue  -- 前台主页数据管理
|    |     |     |     ├── htmlmessage.vue  -- VIP课程html留言
|    |     |     |     ├── jsmessage.vue  -- VIP课程JavaScript留言
|    |     |     |     ├── loginlog.vue  -- 登录日志
|    |     |     |     ├── managemusic.vue  -- 歌曲管理
|    |     |     |     ├── manageusers.vue  -- 用户管理
|    |     |     |     ├── operationlog.vue  -- 操作日志
|    |     |     |     ├── registerlog.vue  -- 注册日志
|    |     |     |     ├── signlog.vue  -- 签到日志
|    |     |     |     ├── specificarticles.vue  -- 账单管理
|    |     |     |     ├── vipcoursecss.vue  -- VIP(css)管理
|    |     |     |     ├── vipcoursehtml.vue  -- VIP(html)管理
|    |     |     |     └── vipcoursejs.vue  -- VIP(js)管理
|    |     ├── coursePage  -- 文章展示
|    |     |     ├── css.vue  -- css文章
|    |     |     ├── html5.vue  -- HTML文章
|    |     |     └── javascript.vue  -- Javascript文章
|    |     ├── userPage  -- 用户页面
|    |     |     ├── aboutme.vue  -- 作者
|    |     |     ├── enjoymusic.vue  -- 欣赏音乐
|    |     |     ├── messagewall.vue  -- 社区留言
|    |     |     ├── userinfo.vue  -- 个人资料
|    |     |     ├── userlogin.vue  -- 用户登录
|    |     |     ├── userregister.vue  -- 用户注册
|    |     |     └── vipcourse.vue  -- 看VIP课程
|    |     └── vipCoursePage  -- VIP课程页面
|    |     |     ├── csscourse.vue  -- css
|    |     |     ├── htmlcourse.vue  -- html
|    |     |     └── javascriptcourse.vue  -- javascript
├── babel.config.js  -- babel配置
└── vue.config.js  -- vue配置文件

<br/>

项目测试介绍

  1. 本项目中后台使用3001端口, 前台使用8080端口。
  2. 你需要在装有Node和Vue的环境中测试,如果其中一个没有请先下载(Node下载,Vue下载).
  3. 下载依赖:npm install 下载后端依赖, 接着进入client, npm install 下载前端依赖.
  4. 配置数据库: 请将打包的数据JSON文件还原到Mongodb数据库中,请对应Mongodb配置的地址和数据库名,你也可以自定义地址和数据库名,一一对应即可.
  5. 以上工作完成后,使用命令node server &命令启动Node服务器,启动成功会显示:
    **Server is running on port [3001].
    **Mongodb is contected.
  6. 进入client,打开命令板,使用命令npm run serve启动vueCli-server,默认使用8080端口,启动成功后用浏览器访问http://localhost:8080,如果首页数据展示成功,则证明数据和程序启动成功。
  7. 本例中将Mongodb部署在本地电脑上,如果你仔细阅读了这篇文档,启动项目应该是很容易的。如果你把Mongodb部署在其他地方,请自行修改config/mongodbURI.js配置文件信息。
  8. 只有完成了上述步骤后再启动项目,不然项目会因为连接不上Mongodb而报错。

准备

passport-jwt和jsonwebtoken

jsonwebtoken是对用户信息加密成不可逆向破解的token.关于passport-jwt,是用来对用户请求时所带的token信息进行过期验证,如果超过签证的合法时间,则会请前台发出token失效的信息,提示用户重新获取合法的token信息,否则无法继续请求加密的信息;

  • jsonwebtoken
// 设置token
// 规则
 const rule = {
    id:String(userinfo._id),
    username:userinfo.username,
    email:userinfo.email,
    date:user.date,
    signdate:userinfo.signdate,
    signcount:userinfo.signcount,
    avatar:userinfo.avatar,
    phone:userinfo.phone
};

// 签证加密
// jwt.sign(规则, key(私钥), {配置:比如过期时长}, (err, token){ 响应程序 })
jwt.sign(rule,key,{expiresIn:7200},(err, token) => {
    if(err) throw err;
    res.json({"token" : "Bearer " + token})
})
  • passport-jwt
const key = require("../config/keys").KEYORSECRET;
const JwtStrategy = require('passport-jwt').Strategy,
      ExtractJwt = require('passport-jwt').ExtractJwt;
var opts = {}
opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken();
opts.secretOrKey = key;

module.exports = passport => {
    passport.use(new JwtStrategy(opts, (jwt_payload, done) => {
        UserInfo.findById(jwt_payload.id)
                .then(user => {
                    if (user) {
                        return done(null, user);
                    } else {
                        return done(null, false);
                        // or you could create a new account
                    }
                })
    }));
}

详细的文档地址:Passport-Jwt合法验证,token加密

<br/>

svg-captcha

svg-captcha验证码的运用,防止暴力破解密码,加强安全性.
详细的文档地址:svg-captcha

使用验证码

// 后台生成验证码
router.get("/getCaptcha", (req, res) => {
    var captcha = svgCaptcha.create({  
        // 翻转颜色  
        inverse: false,  
        // 字体大小  
        fontSize: 38,  
        // 噪声线条数  
        noise: 3,  
        // 宽度  
        width: 80,  
        // 高度  
        height: 32,  
      });  
      // 保存到session,忽略大小写  
      req.session = captcha.text.toLowerCase(); 
      console.log(req.session); //0xtg 生成的验证码
      //保存到cookie 方便前端调用验证
      res.cookie('captcha', req.session); 
      res.setHeader('Content-Type', 'image/svg+xml');
      res.send(String(captcha.data));
      res.end();
})


// 前台获取验证码
--HTML
<img width="80" style="background:#EEE9E9;margin-left:30px;" ref="captcha" height="32" data-original="http://localhost:3001/api/user/getCaptcha" @click="refreshCaptcha">

--js
// 获取验证码cookie
getCookie(cname){
    var name = cname + "=";
    var ca = document.cookie.split(';');
    for(var i=0; i<ca.length; i++){
        var c = ca[i].trim();
        if (c.indexOf(name)==0) return c.substring(name.length,c.length);
    }
    return "";
},
// 刷新验证码
refreshCaptcha(){
    this.$refs.captcha.src = "http://localhost:3001/api/user/getCaptcha?d=" + Math.random();
},

最后用 填写的验证码进行对比


控制台

<br/>

Vue、ElementUI分页使用

关于ElementUI分页详细请见:ElementUI的Pagination分页学习

-- html
<el-pagination
    v-if='paginations.total > 0'
    :page-sizes="paginations.page_sizes"
    :page-size="paginations.page_size"
    :layout="paginations.layout"
    :total="paginations.total"
    :current-page.sync='paginations.page_index'
    @current-change='handleCurrentChange'
    @size-change='handleSizeChange'>
</el-pagination>

-- js
data(){
    return{
        allUsers:[],  // 用来存储最终信息, 被显示的dom点调用
        allTableData:[],  // 用户承接分页设置的数据
        paginations: {   // 分页组件信息
            page_index: 1, // 当前位于哪页
            total: 0, // 总数
            page_size: 5, // 1页显示多少条
            page_sizes: [5, 10, 15, 20], //每页显示多少条
            layout: "total, sizes, prev, pager, next, jumper" // 翻页属性
        },
    }
},
methods:{
    // 获取当前页
    handleCurrentChange(page) {
        let sortnum = this.paginations.page_size * (page - 1);
        let table = this.allTableData.filter((item, index) => {
            return index >= sortnum;
        });
        // 设置默认分页数据
        this.getAllUsers = table.filter((item, index) => {
            return index < this.paginations.page_size;
        });
        this.getAllUsers = table.filter((item, index) => {
            return index < this.paginations.page_size;
        });
    },
    // 切换size
    handleSizeChange(page_size) {
        this.paginations.page_index = 1;
        this.paginations.page_size = page_size;
        this.getAllUsers = this.allTableData.filter((item, index) => {
            return index < page_size;
        });
    },
     // 总页数
    setPaginations() {
        this.paginations.total = this.allTableData.length;
        this.paginations.page_index = 1;
        this.paginations.page_size = 5;
        // 设置默认分页数据
        this.getAllUsers = this.allTableData.filter((item, index) => {
            return index < this.paginations.page_size;
        });
    },
}

没了吗?对,分页就是这么简单!你学会了吗?有些前端开发的同学总是对分页比较陌生,学会这个,让你不再产生烦恼!
对于项目中其他特殊比较复杂的功能,还请您先测试后再进行学习,代码注释很清晰,如果哪里不懂请看下方联系方式

<br/>

项目截图

  • 首页(懒加载)

  • HTML教程

  • 留言墙

(这个根据一个大牛的博客模仿的PC端,大牛博客地址) 移动端是自己写的

  • 欣赏歌曲

  • 搜索歌曲

  • 每日签到

  • 个人信息

  • VIP课程

VIP课程有HTML,CSS,JS课程;VIP用户可以评论留言,可以删除自己的留言;只有用户登录并且付费购买后才能观看.(仅供参考)
  • 搜索页面

有分页功能
  • 404

后台界面

后台首页运用了Echarts,将数据可视化,API是真的强大.更多关于Echarts的学习,百度搜索哦~
  • 添加文章

  • 编辑文章

  • 订单管理

  • 歌曲管理

等等

这里就不再一一贴出项目展示图了, 如果你喜欢,或者想继续在此项目上面扩展, 你可以访问github项目地址

本项目是作者原创, 转载前请留言或联系作者.

如果对你有帮助,请赏个star,谢谢哦~github地址
<br/>

交流

如果大家有兴趣,欢迎star. 欢迎大家加入我的前端交流群:866068198 ,一起交流学习前端技术。博主目前一直在自学Node中,技术有限,如果可以,会尽力给大家提供一些帮助,或是一些学习方法.

联系

If you have some questions after you see this article, you can contact me or you can find some info by clicking these links.

如果对你有帮助,请赏个star,谢谢哦~github地址
查看原文

赞 0 收藏 0 评论 0

达摩师兄 收藏了文章 · 2019-09-19

通过实现25个数组方法来理解及高效使用数组方法(长文,建议收藏)

作者:Maciej Cieslar
译者:前端小智
来源: dev

个人专栏 ES6 深入浅出已上线,深入ES6 ,通过案例学习掌握 ES6 中新特性一些使用技巧及原理,持续更新中,←点击可订阅。

点赞再看,养成习惯

本文 GitHubhttps://github.com/qq44924588... 上已经收录,更多往期高赞文章的分类,也整理了很多我的文档,和教程资料。欢迎Star和完善,大家面试可以参照考点复习,希望我们一起有点东西。


为了保证的可读性,本文采用意译而非直译。

要在给定数组上使用方法,只需要通过[].方法名即可,这些方法都定义在 Array.prototype 对象上。在这里,咱们先不使用这些相,反,咱们将从简单的方法开始定义自己的版本,并在这些版本的基础上进行构建。

没有比把东西拆开再重新组装起来更好的学习方法了。注意,当咱们的实现自己的方法时,不要覆盖现有的方法,因为有的库需要它们,并且这样也方便比较咱们自己的方法与原始方法的差异。

所以不要这样命名咱们自定义的方法:

Array.prototype.map = function map() {
 // implementation
};

最好这样命名:

function map(array) {
 // implementation
}

咱们也可以通过使用class关键字并扩展Array构造函数来实现咱们的方法,如下所示:

class OwnArray extends Array {
 public constructor(...args) {
   super(...args);
 }

 public map() {
   // implementation
   return this;
 }
}

唯一的区别是,我们不使用数组参数,而是使用this关键字。

但是,我觉得 class 方式带来不必要的混乱,所以咱们采用第一种方法。

有了这个,咱们先从实现最简单的方法 forEach 开始!

集合类

.forEach

Array.prototype.forEach 方法对数组的每个元素执行一次提供的函数,而且不会改变原数组。

[1, 2, 3, 4, 5].forEach(value => console.log(value));

实现

function forEach(array, callback) {
  const { length } = array;
  
  for (let index = 0; index < length; index += 1) {
    const value = array[index];
    callback(value, index, array)
  }
}

咱们遍历数组并为每个元素执行回调。这里需要注意的一点是,该方法没有返回什么,所以默认返回undefined

方法涟

使用数组方法的好处是可以将操作链接在一起。考虑以下代码:

function getTodosWithCategory(todos, category) {
 return todos
   .filter(todo => todo.category === category)
   .map(todo => normalizeTodo(todo));
}

这种方式,咱们就不必将map的执行结果保存到变量中,代码会更简洁。

不幸的是,forEach没有返回原数组,这意味着咱们不能做下面的事情

// 无法工作
function getTodosWithCategory(todos, category) {
 return todos
   .filter(todo => todo.category === category)
   .forEach((value) => console.log(value))
   .map(todo => normalizeTodo(todo));
}

帮助函数 (打印信息)

接着实现一个简单的函数,它能更好地解释每个方法的功能:接受什么作为输入,返回什么,以及它是否对数组进行了修改。

function logOperation(operationName, array, callback) {
 const input = [...array];
 const result = callback(array);

 console.log({
   operation: operationName,
   arrayBefore: input,
   arrayAfter: array,
   mutates: mutatesArray(input, array), // shallow check
   result,
 });
}

其中 mutatesArray 方法用来判断是否更改了原数组,如果有修改刚返回 true,否则返回 false。当然大伙有好的想法可以在评论提出呦。

function mutatesArray(firstArray, secondArray) {
  if (firstArray.length !== secondArray.length) {
    return true;
  }

  for (let index = 0; index < firstArray.length; index += 1) {
    if (firstArray[index] !== secondArray[index]) {
      return true;
    }
  }

  return false;
}

然后使用logOperation来测试咱们前面自己实现的 forEach方法。

logOperation('forEach', [1, 2, 3, 4, 5], array => forEach(array, value => console.log(value)));

打印结果:

{
  operation: 'forEach',
  arrayBefore: [ 1, 2, 3, 4, 5 ],
  arrayAfter: [ 1, 2, 3, 4, 5 ],
  mutates: false,
  result: undefined
}


.map

map 方法会给原数组中的每个元素都按顺序调用一次 callback 函数。callback 每次执行后的返回值(包括 undefined)组合起来形成一个新数组。

实现

function map(array, callback) {
  const result = [];
  const { length } = array;
  
  for (let index = 0; index < length; index +=1) {
    const value = array[index];
    
    result[index] = callback(value, index, array);
  }

  return result;
}

提供给方法的回调函数接受旧值作为参数,并返回一个新值,然后将其保存在新数组中的相同索引下,这里用变量 result 表示。

这里需要注意的是,咱们返回了一个新的数组,不修改旧的。

测试

logOperation('map', [1, 2, 3, 4, 5], array => map(array, value => value + 5));

打印结果:

{ 
  operation: 'map',
  arrayBefore: [ 1, 2, 3, 4, 5 ],
  arrayAfter: [ 1, 2, 3, 4, 5 ],
  mutates: false,
  result: [ 6, 7, 8, 9, 10 ]
 }

.filter

Array.prototype.filter 过滤回调返回为false的值,每个值都保存在一个新的数组中,然后返回。

[1, 2, 3, 4, 5].filter(number => number >= 3);
// -> [3, 4, 5]

实现

function push(array, ...values) {
  const { length: arrayLength } = array;
  const { length: valuesLength } = values;

  for (let index = 0; index < valuesLength; index += 1) {
    array[arrayLength + index] = values[index];
  }

  return array.length;
}
--------------------------------------------------
function filter(array, callback) {
 const result = [];

 const { length } = array;

 for (let index = 0; index < length; index += 1) {
   const value = array[index];

   if (callback(value, index, array)) {
     push(result, value);
   }
 }

 return result;
}

获取每个值并检查所提供的回调函数是否返回truefalse,然后将该值添加到新创建的数组中,或者适当地丢弃它。

注意,这里对result 数组使用push方法,而不是将值保存在传入数组中放置的相同索引中。这样,result就不会因为丢弃的值而有空槽。

测试

logOperation('filter', [1, 2, 3, 4, 5], array => filter(array, value => value >= 2));

运行:

{ 
  operation: 'filter',
  arrayBefore: [ 1, 2, 3, 4, 5 ],
  arrayAfter: [ 1, 2, 3, 4, 5 ],
  mutates: false,
  result: [ 2, 3, 4, 5 ] 
}

.reduce

reduce() 方法接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终计算为一个值reduce() 方法接受四个参数:初始值(或者上一次回调函数的返回值),当前元素值,当前索引,调用 reduce() 的数组

确切地说,如何计算该值是需要在回调中指定的。来看呓使用reduce的一个简单的例子:对一组数字求和:

 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].reduce((sum, number) => {
   return sum + number;
 }, 0) // -> 55

注意这里的回调接受两个参数:sumnumber。第一个参数总是前一个迭代返回的结果,第二个参数在遍历中的当前数组元素。

这里,当咱们对数组进行迭代时,sum包含到循环当前索引的所有数字的和因为每次迭代咱们都将数组的当前值添加到sum中。

实现

function reduce(array, callback, initValue) {
  const { length } = array;
  
  let acc = initValue;
  let startAtIndex = 0;

  if (initValue === undefined) {
    acc = array[0];
    startAtIndex = 0;
  }

  for (let index = startAtIndex; index < length; index += 1) {
    const value = array[index];
    acc = callback(acc, value, index, array)
  }
 
  return acc;
}

咱们创建了两个变量accstartAtIndex,并用它们的默认值初始化它们,分别是参数initValue0

然后,检查initValue是否是undefined。如果是,则必须将数组的第一个值设置为初值,为了不重复计算初始元素,将startAtIndex设置为1

每次迭代,reduce方法都将回调的结果保存在累加器(acc)中,然后在下一个迭代中使用。对于第一次迭代,acc被设置为initValuearray[0]

测试

logOperation('reduce', [1, 2, 3, 4, 5], array => reduce(array, (sum, number) => sum + number, 0));

运行:

{ operation: 'reduce',
  arrayBefore: [ 1, 2, 3, 4, 5 ],
  arrayAfter: [ 1, 2, 3, 4, 5 ],
  mutates: false,
  result: 15 
}

检索类

有什么操作比搜索特定值更常见?这里有一些方法可以帮助我们。

.findIndex

findIndex帮助咱们找到数组中给定值的索引。

[1, 2, 3, 4, 5, 6, 7].findIndex(value => value === 5); // 4

findIndex方法对数组中的每个数组索引0..length-1(包括)执行一次callback函数,直到找到一个callback函数返回真实值(强制为true)的值。如果找到这样的元素,findIndex会立即返回该元素的索引。如果回调从不返回真值,或者数组的length0,则findIndex返回-1

实现

function findIndex(array, callback) {
 const { length } = array;

 for (let index = 0; index < length; index += 1) {
   const value = array[index];

   if (callback(value, index, array)) {
     return index;
   }
 }

 return -1;
}

测试

logOperation('findIndex', [1, 2, 3, 4, 5], array => findIndex(array, number => number === 3));

运行:

{
  operation: 'findIndex',
  arrayBefore: [ 1, 2, 3, 4, 5 ],
  arrayAfter: [ 1, 2, 3, 4, 5 ],
  mutates: false,
  result: 2
}

.find

findfindIndex的唯一区别在于,它返回的是实际值,而不是索引。实际工作中,咱们可以重用已经实现的findIndex

[1, 2, 3, 4, 5, 6, 7].find(value => value === 5); // 5

实现

function find(array, callback) {
 const index = findIndex(array, callback);

 if (index === -1) {
   return undefined;
 }

 return array[index];
}

测试

logOperation('find', [1, 2, 3, 4, 5], array => find(array, number => number === 3));

运行

{
  operation: 'find',
  arrayBefore: [ 1, 2, 3, 4, 5 ],
  arrayAfter: [ 1, 2, 3, 4, 5 ],
  mutates: false,
  result: 3
}

.indexOf

indexOf是获取给定值索引的另一种方法。然而,这一次,咱们将实际值作为参数而不是函数传递。同样,为了简化实现,可以使用前面实现的findIndex

[3, 2, 3].indexOf(3); // -> 0

实现

function indexOf(array, searchedValue) {
  return findIndex(array, value => value === searchedValue)
}

测试

logOperation('indexOf', [1, 2, 3, 4, 5], array => indexOf(array, 3));

执行结果

{
  operation: 'indexOf',
  arrayBefore: [ 1, 2, 3, 4, 5 ],
  arrayAfter: [ 1, 2, 3, 4, 5 ],
  mutates: false,
  result: 2
}

.lastIndexOf

lastIndexOf的工作方式与indexOf相同,lastIndexOf() 方法返回指定元素在数组中的最后一个的索引,如果不存在则返回 -1

[3, 2, 3].lastIndexOf(3); // -> 2

实现

function lastIndexOf(array, searchedValue) {
  for (let index = array.length - 1; index > -1; index -= 1 ){
    const value = array[index];
    
    if (value === searchedValue) {
      return index;
    }
  }
  return  -1;
}

代码基本与findIndex类似,但是没有执行回调,而是比较valuesearchedValue。如果比较结果为 true,则返回索引,如果找不到值,返回-1

测试

logOperation('lastIndexOf', [1, 2, 3, 4, 5, 3], array => lastIndexOf(array, 3));

执行结果

{ 
  operation: 'lastIndexOf',
  arrayBefore: [ 1, 2, 3, 4, 5, 3 ],
  arrayAfter: [ 1, 2, 3, 4, 5, 3 ],
  mutates: false,
  result: 5 
}

.every

every() 方法测试一个数组内的所有元素是否都能通过某个指定函数的测试,它返回一个布尔值。

[1, 2, 3].every(value => Number.isInteger(value)); // -> true

咱们可以将every 方法看作一个等价于逻辑与的数组。

实现

function every(array, callback){
  const { length } = array;
  
  for (let index = 0; index < length; index += 1) {
   const value = array[index];
   
    if (!callback(value, index, array)) {
      return false;
    }
  }

  return true;
}

咱们为每个值执行回调。如果在任何时候返回false,则退出循环,整个方法返回false。如果循环终止而没有进入到if语句里面(说明条件都成立),则方法返回true

测试

logOperation('every', [1, 2, 3, 4, 5], array => every(array, number => Number.isInteger(number)));

执行结果

{
  operation: 'every',
  arrayBefore: [ 1, 2, 3, 4, 5 ],
  arrayAfter: [ 1, 2, 3, 4, 5 ],
  mutates: false,
  result: true 
}

.some

some 方法与 every 刚好相反,即只要其中一个为true 就会返回true。与every 方法类似,咱们可以将some 方法看作一个等价于逻辑或数组。

[1, 2, 3, 4, 5].some(number => number === 5); // -> true

实现

function some(array, callback) {
 const { length } = array;

 for (let index = 0; index < length; index += 1) {
   const value = array[index];

   if (callback(value, index, array)) {
     return true;
   }
 }

 return false;
}

咱们为每个值执行回调。如果在任何时候返回true,则退出循环,整个方法返回true。如果循环终止而没有进入到if语句里面(说明条件都不成立),则方法返回false

测试

logOperation('some', [1, 2, 3, 4, 5], array => some(array, number => number === 5));

执行结果

{
  operation: 'some',
  arrayBefore: [ 1, 2, 3, 4, 5 ],
  arrayAfter: [ 1, 2, 3, 4, 5 ],
  mutates: false,
  result: true
}

.includes

includes方法的工作方式类似于 some 方法,但是includes不用回调,而是提供一个参数值来比较元素。

[1, 2, 3].includes(3); // -> true

实现

function includes(array, searchedValue){
  return some(array, value => value === searchedValue)
}

测试

logOperation('includes', [1, 2, 3, 4, 5], array => includes(array, 5));

执行结果

{
  operation: 'includes',
  arrayBefore: [ 1, 2, 3, 4, 5 ],
  arrayAfter: [ 1, 2, 3, 4, 5 ],
  mutates: false,
  result: true
}

拼接、附加和反转数组

.concat

concat() 方法用于合并两个或多个数组,此方法不会更改现有数组,而是返回一个新数组。

[1, 2, 3].concat([4, 5], 6, [7, 8]) // -> [1, 2, 3, 4, 5, 6, 7, 8]

实现

function concat(array, ...values) {
  const result = [...array];
  const { length } = values;

  for (let index = 0; index < length; index += 1) {
    const value = values[index];
    
    if (Array.isArray(value)) {
      push(result, ...value);
    } else {
      push(result, value);
    }
  }

  return result;
}

concat将数组作为第一个参数,并将未指定个数的值作为第二个参数,这些值可以是数组,也可以是其他类型的值。

首先,通过复制传入的数组创建 result 数组。然后,遍历 values ,检查该值是否是数组。如果是,则使用push函数将其值附加到结果数组中。

push(result, value) 只会向数组追加为一个元素。相反,通过使用展开操作符push(result,…value) 将数组的所有值附加到result 数组中。在某种程度上,咱们把数组扁平了一层。

测试

logOperation('concat', [1, 2, 3, 4, 5], array => concat(array, 1, 2, [3, 4]));

执行结果

{ 
 operation: 'concat',
  arrayBefore: [ 1, 2, 3, 4, 5 ],
  arrayAfter: [ 1, 2, 3, 4, 5 ],
  mutates: false,
  result: [ 1, 2, 3, 4, 5, 1, 2, 3, 4 ] 
}

.join

join() 方法用于把数组中的所有元素放入一个字符串,元素是通过指定的分隔符进行分隔的。

['Brian', 'Matt', 'Kate'].join(', ') // -> Brian, Matt, Kate

实现

function join(array, joinWith) {
  return reduce(
    array,
    (result, current, index) => {
      if (index === 0) {
        return current;
      }
      
      return `${result}${joinWith}${current}`;
    },
    ''
  )
}

reduce的回调是神奇之处:reduce遍历所提供的数组并将结果字符串拼接在一起,在数组的值之间放置所需的分隔符(作为joinWith传递)。

array[0]值需要一些特殊的处理,因为此时result是一个空字符串,而且咱们也不希望分隔符(joinWith)位于第一个元素前面。

测试

logOperation('join', [1, 2, 3, 4, 5], array => join(array, ', '));

执行结果

{
  operation: 'join',
  arrayBefore: [ 1, 2, 3, 4, 5 ],
  arrayAfter: [ 1, 2, 3, 4, 5 ],
  mutates: false,
  result: '1, 2, 3, 4, 5'
}

.reverse

reverse() 方法将数组中元素的位置颠倒,并返回该数组,该方法会改变原数组。

实现

function reverse(array) {
  const result = []
  const lastIndex = array.length - 1;

  for (let index = lastIndex; index > -1; index -= 1) {
    const value = array[index];
    result[lastIndex - index ] = value
  }
  return result;
}

其思路很简单:首先,定义一个空数组,并将数组的最后一个索引保存为变量(lastIndex)。接着反过来遍历数组,将每个值保存在结果result 中的(lastIndex - index)位置,然后返回result数组。

测试

logOperation('reverse', [1, 2, 3, 4, 5], array => reverse(array));

执行结果

{
  operation: 'reverse',
  arrayBefore: [ 1, 2, 3, 4, 5 ],
  arrayAfter: [ 1, 2, 3, 4, 5 ],
  mutates: false,
  result: [ 5, 4, 3, 2, 1 ]
}

添加、删除和追加值

.shift

shift() 方法从数组中删除第一个元素,并返回该元素的值,此方法更改数组的长度。

[1, 2, 3].shift(); // -> 1

实现

function shift(array) {
  const { length } = array;
  const firstValue = array[0];

  for (let index = 1; index > length; index += 1) {
    const value = array[index];
    array[index - 1] = value;
  }

  array.length = length - 1;

  return firstValue;
}

首先保存数组的原始长度及其初始值,然后遍历数组并将每个值向下移动一个索引。完成遍历后,更新数组的长度并返回初始值。

测试

logOperation('shift', [1, 2, 3, 4, 5], array => shift(array));

执行结果

{
  operation: 'shift',
  arrayBefore: [ 1, 2, 3, 4, 5 ],
  arrayAfter: [ 2, 3, 4, 5 ],
  mutates: true,
  result: 1
}

.unshift

unshift() 方法将一个或多个元素添加到数组的开头,并返回该数组的新长度(该方法修改原有数组)。

[2, 3, 4].unshift(1); // -> [1, 2, 3, 4]

实现

function unshift(array, ...values) {
  const mergedArrays = concat(values, ...array);
  const { length: mergedArraysLength } = mergedArrays;

  for (let index = 0; index < mergedArraysLength; index += 1) {
    const value = mergedArrays[index];
    array[index] = value;
  }

  return array.length;
}

首先将需要加入数组(作为参数传递的单个值)和数组拼接起来。这里需要注意的是values 放在第一位的,也就是放置在原始数组的前面。

然后保存这个新数组的长度并遍历它,将它的值保存在原始数组中,并覆盖开始时的值。

测试

logOperation('unshift', [1, 2, 3, 4, 5], array => unshift(array, 0));

执行结果

{
  operation: 'unshift',
  arrayBefore: [ 1, 2, 3, 4, 5 ],
  arrayAfter: [ 0, 1, 2, 3, 4, 5 ],
  mutates: true,
  result: 6
}

.slice

slice() 

方法返回一个新的数组对象,这一对象是一个由 beginend 决定的原数组的浅拷贝(包括 begin,不包括end)原始数组不会被改变。

slice 会提取原数组中索引从 beginend 的所有元素(包含 begin,但不包含 end)。

[1, 2, 3, 4, 5, 6, 7].slice(3, 6); // -> [4, 5, 6]

实现 (简单实现)

function slice(array, startIndex = 0, endIndex = array.length) {
 const result = [];

 for (let index = startIndex; index < endIndex; index += 1) {
   const value = array[index];

   if (index < array.length) {
     push(result, value);
   }
 }

 return result;
}

咱们遍历数组从startIndexendIndex,并将每个值放入result。这里使用了这里的默认参数,这样当没有传递参数时,slice方法只创建数组的副本。

注意:if语句确保只在原始数组中存在给定索引下的值时才加入 result 中。

测试

logOperation('slice', [1, 2, 3, 4, 5], array => slice(array, 1, 3));

执行结果

{
  operation: 'slice',
  arrayBefore: [ 1, 2, 3, 4, 5 ],
  arrayAfter: [ 1, 2, 3, 4, 5 ],
  mutates: false,
  result: [ 2, 3 ]
}

.splice

splice() 方法通过删除或替换现有元素或者原地添加新的元素来修改数组,并以数组形式返回被修改的内容。此方法会改变原数组。

首先,指定起始索引,然后指定要删除多少个值,其余的参数是要插入的值。

const arr = [1, 2, 3, 4, 5];
// 从位置0开始,删除2个元素后插入 3, 4, 5
arr.splice(0, 2, 3, 4, 5);

arr // -> [3, 4, 5, 3, 4, 5]

实现

function splice( array, insertAtIndex, removeNumberOfElements, ...values) {
  const firstPart = slice(array, 0, insertAtIndex);
  const secondPart = slice(array, insertAtIndex + removeNumberOfElements);

  const removedElements = slice(
    array,
    insertAtIndex,
    insertAtIndex + removeNumberOfElements
  );

  const joinedParts = firstPart.concat(values, secondPart);
  const { length: joinedPartsLength } = joinedParts;

  for (let index = 0; index < joinedPartsLength; index += 1) {
    array[index] = joinedParts[index];
  }

  array.length = joinedPartsLength;

  return removedElements;
}

其思路是在insertAtIndexinsertAtIndex + removeNumberOfElements上进行两次切割。这样,将原始数组切成三段。第一部分(firstPart)和第三部分(secondPart)加个插入的元素组成为最后数组的内容。

测试

logOperation('splice', [1, 2, 3, 4, 5], array => splice(array, 1, 3));

执行结果

{
  operation: 'splice',
  arrayBefore: [ 1, 2, 3, 4, 5 ],
  arrayAfter: [ 1, 5 ],
  mutates: true,
  result: [ 2, 3, 4 ]
}

.pop

pop()方法从数组中删除最后一个元素,并返回该元素的值。此方法更改数组的长度。

实现

function pop(array) {
 const value = array[array.length - 1];

 array.length = array.length - 1;

 return value;
}

首先,将数组的最后一个值保存在一个变量中。然后只需将数组的长度减少1,从而删除最后一个值。

测试

logOperation('pop', [1, 2, 3, 4, 5], array => pop(array));

执行结果

{
  operation: 'pop',
  arrayBefore: [ 1, 2, 3, 4, 5 ],
  arrayAfter: [ 1, 2, 3, 4 ],
  mutates: true,
  result: 5
}

.push

push() 方法将一个或多个元素添加到数组的末尾,并返回该数组的新长度。

[1, 2, 3, 4].push(5); // -> [1, 2, 3, 4, 5]

实现

function push(array, ...values) {
  const { length: arrayLength } = array;
  const { length: valuesLength } = values;

  for (let index = 0; index < valuesLength; index += 1) {
    array[arrayLength + index] = values[index];
  }

  return array.length;
}

首先,我们保存原始数组的长度,以及在它们各自的变量中要添加的值。然后,遍历提供的值并将它们添加到原始数组中。

测试

logOperation('push', [1, 2, 3, 4, 5], array => push(array, 6, 7));

执行结果

{
  operation: 'push',
  arrayBefore: [ 1, 2, 3, 4, 5 ],
  arrayAfter: [
    1, 2, 3, 4,5, 6, 7
  ],
  mutates: true,
  result: 7
}

.fill

当咱们想用一个占位符值填充一个空数组时,可以使用fill方法。如果想创建一个指定数量的null元素数组,可以这样做:

[...Array(5)].fill(null) // -> [null, null, null, null, null]

实现

function fill(array, value, startIndex = 0, endIndex = array.length) {
 for (let index = startIndex; index < endIndex; index += 1) {
   array[index] = value;
 }

 return array;
}

fill方法真正做的是替换指定索引范围内的数组的值。如果没有提供范围,该方法将替换所有数组的值。

测试

logOperation("fill", [...new Array(5)], array => fill(array, 0));

执行结果

{
  operation: 'fill',
  arrayBefore: [ undefined, undefined, undefined, undefined, undefined ],
  arrayAfter: [ 0, 0, 0, 0, 0 ],
  mutates: true,
  result: [ 0, 0, 0, 0, 0 ]
}

扁平类

有时咱们的数组会变嵌套两到三层,咱们想要将它们扁,也就是减少嵌套的程度。例如,想将所有值都放到顶层。为咱们提供帮助有两个新特性:flatflatMap 方法。

.flat

flat方法通过可指定深度值来减少嵌套的深度。

[1, 2, 3, [4, 5, [6, 7, [8]]]].flat(1); // -> [1, 2, 3, 4, 5, [6, 7, [8]]]

因为展开的深度值是1,所以只有第一级数组是被扁平,其余的保持不变。

[1, 2, 3, [4, 5]].flat(1) // -> [1, 2, 3, 4, 5]

实现

function flat(array, depth = 0) {
 if (depth < 1 || !Array.isArray(array)) {
   return array;
 }

 return reduce(
   array,
   (result, current) => {
     return concat(result, flat(current, depth - 1));
   },
   [],
 );
}

首先,我们检查depth参数是否小于1。如果是,那就意味着没有什么要扁平的,咱们应该简单地返回数组。

其次,咱们检查数组参数是否属于数组类型,因为如果它不是,那么扁化就没有意义了,所以只返回这个参数。

咱们们使用了之前实现的reduce函数。从一个空数组开始,然后取数组的每个值并将其扁平。

注意,我们调用带有(depth - 1)flat函数。每次调用时,都递减depth参数,以免造成无限循环。扁平化完成后,将返回值来回加到result数组中。

测试

logOperation('flat', [1, 2, 3, [4, 5, [6]]], array => flat(array, 2));

执行结果

{
  operation: 'flat',
  arrayBefore: [ 1, 2, 3, [ 4, 5, [Array] ] ],
  arrayAfter: [ 1, 2, 3, [ 4, 5, [Array] ] ],
  mutates: false,
  result: [ 1, 2, 3, 4, 5, 6 ]
}

.flatMap

flatMap() 方法首先使用映射函数映射每个元素,然后将结果压缩成一个新数组。它与 map 和 深度值1的 flat 几乎相同,但 flatMap 通常在合并成一种方法的效率稍微高一些。

在上面的map方法中,对于每个值,只返回一个值。这样,一个包含三个元素的数组在映射之后仍然有三个元素。使用flatMap,在提供的回调函数中,可以返回一个数组,这个数组稍后将被扁平。

[1, 2, 3].flatMap(value => [value, value, value]); // [1, 1, 1, 2, 2, 2, 3, 3, 3]

每个返回的数组都是扁平的,我们得到的不是一个嵌套了三个数组的数组,而是一个包含9个元素的数组。

实现

function flatMap(array, callback) {
 return flat(map(array, callback), 1);
}

首先使用map,然后将数组的结果数组扁平化一层。

测试

logOperation('flatMap', [1, 2, 3], array => flatMap(array, number => [number, number]));

执行结果

{
  operation: 'flatMap',
  arrayBefore: [ 1, 2, 3 ],
  arrayAfter: [ 1, 2, 3 ],
  mutates: false,
  result: [ 1, 1, 2, 2, 3, 3 ]
}

generator 类

最后三种方法的特殊之处在于它们返回生成器的方式。如果你不熟悉生成器,请跳过它们,因为你可能不会很快使用它们。

.values

values方法返回一个生成器,该生成器生成数组的值。

const valuesGenerator = values([1, 2, 3, 4, 5]);

valuesGenerator.next(); // { value: 1, done: false }

实现

function values(array) {
 const { length } = array;

 function* createGenerator() {
   for (let index = 0; index < length; index += 1) {
     const value = array[index];
     yield value;
   }
 }

 return createGenerator();
}

首先,咱们定义createGenerator函数。在其中,咱们遍历数组并生成每个值。

.keys

keys方法返回一个生成器,该生成器生成数组的索引。

const keysGenerator = keys([1, 2, 3, 4, 5]);

keysGenerator.next(); // { value: 0, done: false }

实现

function keys(array) {
 function* createGenerator() {
   const { length } = array;

   for (let index = 0; index < length; index += 1) {
     yield index;
   }
 }

 return createGenerator();
}

实现完全相同,但这一次,生成的是索引,而不是值。

.entries

entry方法返回生成键值对的生成器。

const entriesGenerator = entries([1, 2, 3, 4, 5]);

entriesGenerator.next(); // { value: [0, 1], done: false }

实现

function entries(array) {
 const { length } = array;

 function* createGenerator() {
   for (let index = 0; index < length; index += 1) {
     const value = array[index];
     yield [index, value];
   }
 }

 return createGenerator();
}

同样的实现,但现在咱们将索引和值结合起来,并在数组中生成它们。

总结

高效使用数组的方法是成为一名优秀开发人员的基础。了解他们内部工作的复杂性是我所知道的最好的方法。

代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

原文:https://dev.to/bnevilleoneill...

交流(欢迎加入群,群工作日都会发红包,互动讨论技术)

阿里云最近在做活动,低至2折,有兴趣可以看看:https://promotion.aliyun.com/...

干货系列文章汇总如下,觉得不错点个Star,欢迎 加群 互相学习。

https://github.com/qq44924588...

我是小智,公众号「大迁世界」作者,对前端技术保持学习爱好者。我会经常分享自己所学所看的干货,在进阶的路上,共勉!

关注公众号,后台回复福利,即可看到福利,你懂的。

clipboard.png

每次整理文章,一般都到2点才睡觉,一周4次左右,挺苦的,还望支持,给点鼓励

查看原文

达摩师兄 赞了文章 · 2019-09-18

纯前端音乐播放器,极简

demo:https://chu295.github.io/295/
项目地址:https://github.com/CHU295/chu...

图片描述

数据来源于咪咕音乐

查看原文

赞 11 收藏 7 评论 9

达摩师兄 关注了收藏夹 · 2019-09-18

后端面试题汇总,面试经验分享

关注 597

达摩师兄 关注了用户 · 2019-09-18

花裤衩 @panjiachen

show me the code

关注 2549