TiJay

TiJay 查看完整档案

北京编辑南开大学  |  软件工程 编辑京东  |  前端工程师 编辑 tong-jing.github.io/ 编辑
编辑

In me, past, present, future meet...
In me, the tiger sniffs the rose...
.....∵∴★.∴∵∴╭╯╭╯╭╯╭╯∴∵∴∵∴
..☆.∵∴∵.∴∵∴▍▍▍▍▍▍▍▍☆★∵∴
.▍.∴∵∴∵.∴▅██████████☆★∵
.◥█▅▅▅▅████▅█▅█▅█▅█▅█▅████◤
. ◥████████████████████◤
....◥████████████████◤

个人动态

TiJay 发布了文章 · 2019-10-21

【Webpack】一些配置优化与解决方案

开始

官网是最好的学习资料,本篇文章略过入门配置这些内容,整理了一些常用的配置点。

在 webpack 打包过程查询的依赖关系:

  • ES2015 import 语句
  • CommonJS require() 语句
  • AMD define 和 require 语句
  • css/sass/less 文件中的 @import 语句。
  • 样式(url(...))或 HTML 文件(<img data-original=...>)中的图片链接

从入口起点(entry) 开始,webpack 根据文件的这些依赖递归地构建一个依赖图 ,将这个依赖图中所有这些模块打包输出(output)为少量的 bundle 到 /dist 或 /build 目录中。

一、module 的加载

webpack 默认从入口开始,一切文件都是模块,通过 module 配置处理各种类型的文件(js, css, sass, jpg, png....)。

加载 webpack 依赖图中的不同类型模块,需要不同的预处理器(loader)。果不加loader 会有“ ModuleParseError: Module parse failed "等错误。

需要在 module.rules 中为各类文件配置 loader,例如加载 css 和图片时 loader 安装和配置:

npm install --save-dev style-loader css-loader file-loader
// webpack.config.js

  const path = require('path');

  module.exports = {
    entry: './src/index.js',
    output: {
      filename: 'bundle.js',
      path: path.resolve(__dirname, 'dist')
    },
    module: {
      rules: [
        {
          test: /\.css$/,
          use: ['style-loader','css-loader']
        },
        {
          test: /\.(png|svg|jpg|gif)$/,
          use: ['file-loader']
        }
      ]
    }
  };

在打包中遇到非JS模块时,会在 module.rules 规则中匹配文件后缀名, 然后用 Rules.use 指定的的预处理器(loader)解析该文件。use 加载器可以链式传递,从右向左进行应用到模块上

可以尝试打包自定义文件类型,只需要安装配置对应的预处理器(loader)。

忽略匹配的文件

有一种场景是项目依赖的资源不需要加载解析,例如一些地图的API或JS库。

这时可以使用 module.noParse 防止 webpack 解析与正则表达式相匹配的文件,忽略的文件中的 import, require, define 等导入机制,配置方式如下:

  module: {
    noParse: /jquery|lodash/,
    
    // 从 webpack 3.0 开始,可以使用函数,如下所示:
    // noParse: function(content) {
    //   return /jquery|lodash/.test(content);
    // }
  }

忽略大型的 library 可以提高构建性能。

二、优化 CSS 相关配置

2.1 PostCss 预处理

PostCss 是一个 CSS 的预处理工具,可以帮助我们给 CSS3 的属性添加前缀(autoprefixer),样式格式校验(stylelint),提前使用 css 的新特性(postcss-cssnext),更重要的是可以实现 CSS 的模块化,防止 CSS 样式冲突。

安装 postcss-loader 和 一些 PostCss 的插件:

npm i -D postcss-loader
npm i -D autoprefixer postcss-cssnext

可以给 postcss-loader 设置多个插件:

rules: [
      {
        test: /\.(sc|c|sa)ss$/,
        use: ['style-loader', 'css-loader', 'sass-loader',
          {
            loader: "postcss-loader",
            options: {
              ident: 'postcss',
              sourceMap: true,
              plugins: loader => [
                require('autoprefixer')(),
                require('postcss-cssnext')()
              ]
            }
          }
        ]
      }
    ]

PostCss 还有很多丰富的插件可以使用。

2.2 样式拆分提取

在生产环境下将样式表抽离成专门的单独文件,使每个包含css的js文件都会创建一个CSS文件,支持按需加载css。

webpack4 使用 mini-css-extract-plugin 插件拆分样式, webpack3 之前版本可以用 extract-text-webpack-plugin 插件:

npm i -D mini-css-extract-plugin

使用 mini-css-extract-plugin 就不能再用 style-loader 注入到 html 中了。除了修改 module 配置,还需要修改plugins配置:

// webpack.product.config.js
  module: {
    rules: [ // 规则数组,修改模块的创建方式
      {
        test: /\.(sc|c|sa)ss$/,  // 正则表达式,处理scss  sass css
        use: [
          {
              MiniCssExtractPlugin.loader,
              options: {
              // 这里可以指定一个 publicPath
              // 默认使用 webpackOptions.output中的publicPath
              publicPath: '../'
             }
          },
          'css-loader',   
  //...        
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name].css', // 设置最终输出的文件名
      chunkFilename: '[id].css'
    })
  ]

其中的 name 是配置 output.filename 时的 name,
可以配置 pakeage.json 中的 script 命令运行生产环境下的打包配置,使用--config 指定webpack的执行脚本

 "scripts": {
    "build": "npx webpack",
    "dist": "npx webpack --config webpack.product.config.js"
  }

打包后新增 dist/main.css,而 html 中的样式失效了,因为没有 style 注入了,这时需要手动在 html 中引入 main.css 文件。

注意这个插件应该只用在 mode: 'production' 配置中,并且在 loaders 链中不能使用 style-loader, 特别是不支持开发时的 HMR。

对于 JS 代码拆分可以使用 CommonsChunkPlugin 插件,配置方式都类似。

2.3 文件压缩

webpack4 可以使用插件 optimize-css-assets-webpack-plugin 压缩文件,注意 webpack4 才有minimizer这个配置项:

npm i -D optimize-css-assets-webpack-plugin
// webpack.product.config.js
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');

module.exports = {
//...
  optimization: {
    minimizer: [new OptimizeCSSAssetsPlugin({})] // // 压缩 css 文件
  },
  plugins: [
    new MiniCssExtractPlugin({ // 拆分 CSS
      filename: "[name].css",
      chunkFilename: "[id].css"
    }),
  ],
 }

同理压缩 JS 也需要一个插件 uglifyjs-webpack-plugin, 这些压缩插件插件需要一个前提就是:mode: 'production':

npm i -D uglifyjs-webpack-plugin
// webpack.product.config.js
+ const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); // 压缩js

module.exports = {
  ...
  optimization: {
    minimizer: [
+     new UglifyJsPlugin({ // 压缩 js
        cache: true,
        parallel: true,
        sourceMap: true // set to true if you want JS source maps
      }),
      new OptimizeCSSAssetsPlugin({}) // 压缩 css 文件
    ]
  }
  ···
};

这里需要注意的是,如果没有 bable 兼容 ES6 语法,则会报" ERROR Unexpected token "等。

三、图片处理与优化

3.1 加载图片与字体

使用file-loader处理文件的导入:

npm install --save-dev file-loader
module.exports = {
  ...
  module: {
    rules: [ 
      ...
      {
        test: /\.(png|svg|jpg|gif)$/,
        use: ['file-loader']
      }
      ···
    ]  
  } 
}     

由于 css 中可能引用到自定义的字体,处理也是跟图片一致。

test: /\.(woff|woff2|eot|ttf|otf)$/,
use: [ 'file-loader' ]

3.2 压缩图片

file-loader将图片拷贝到dist目录并更新引用路径,进一步使用 image-webpack-loader 对图片进行压缩和优化,按照 NPM 官网文档进行安装配置:

npm install image-webpack-loader --save-dev
module.exports = {
  ...
  module: {
    rules: [ 
      ...
      {
        test: /\.(png|svg|jpg|gif)$/,
        use: ['file-loader',
          {
            loader: 'image-webpack-loader',
            options: {
              mozjpeg: {
                progressive: true,
                quality: 65
              },
              // optipng.enabled: false will disable optipng
              optipng: {
                enabled: false,
              },
              pngquant: {
                quality: '65-90',
                speed: 4
              },
              gifsicle: {
                interlaced: false,
              },
              // the webp option will enable WEBP
              webp: {
                quality: 75
              }
            }
          }
        ]
      }
     ···
    ]  
  } 
}

原始图片content-length: 557478,大概557K,重新打包压缩后171K

3.3 小图片处理为 base64 减少 http 请求

url-loader 功能类似于 file-loader,可以把 url 地址对应的文件,打包成 base64 的 Data URI scheme,提高访问的效率。使用base64编码的图片和超链接方式的代码分别如下:

<img data-original="data:image/gif;base64,R0lGODlhAwADAIABAL6+vv///yH5BAEAAAEALAAAAAADAAMAAAIDjA9WADs=" />

<img data-original="http://gpeng.win/test.png" />

对于比较小的图片可以直接打包成 base64 从而减少http请求次数,在最新版的浏览器特别是移动端对base64的兼容性都非常好,可以放心使用。

npm install --save-dev url-loader
module.exports = {
  ...
  module: {
    rules: [ 
      ...
      {
        test: /\.(png|svg|jpg|gif)$/,
        use: [
          // 'file-loader',
          {
            loader: 'url-loader', // 根据图片大小,把图片优化成base64
            options: {
              limit: 10000 // 10KB
            }
          },
          ···
        ]
      }
      ···
    ]  
  } 
}     

四、解决缓存问题

4.1 配置文件 Hash 名

因为浏览器的缓存策略,如果我们在部署新版本时不更改资源的文件名,浏览器可能会认为它没有被更新,就会使用它的缓存版本,所以在打包新版本后浏览器可能依旧使用之前的文件。

解决缓存的问题,一种方式就是修改文件名,每次打包文件都更新文件名。在出口(output)和管理输出的 plugins 中配置文件的 Hash 名:

module.exports = {
  mode: 'production',
  entry: './src/index.js',
  output: {
    filename: 'main.[hash].js',
    path: path.resolve(__dirname, './dist')
  },
  ···  
  plugins: [
    new MiniCssExtractPlugin({ // 拆分css
      filename: '[name].[hash].css', // 设置最终输出的文件名
      chunkFilename: '[id].[hash].css'
    })
  ],
  ···  
};

修改filename: '[name].[hash].css'

> npx webpack --config webpack.product.config.js
Hash: 10c0c7348792960894f6
Version: webpack 4.30.0

这时 dist/main.10c0c7348792960894f6.css出现。

4.2 文件 Hash 名的注入

在前面拆分CSS或JS为单独文件时,HTML是手动引入的,但Hash生成的文件每次都不同,要如何自动引入?

使用 HtmlWebpackPlugin 插件,可以把打包后的 CSS 或者 JS 文件引用直接注入到 HTML 模板中,这样就不用每次手动修改文件引用了。

npm i -D html-webpack-plugin
+ const HtmlWebpackPlugin = require('html-webpack-plugin');
···
module.exports = {
  mode: 'production',
  ···  
  plugins: [
    ···
+   new HtmlWebpackPlugin({
      title: 'Learn webpack', // 默认值:Webpack App
      filename: 'index.html', // 最终生成的文件,默认值: 'index.html'
      template: path.resolve(__dirname, 'src/index.html'), // 模版
      minify: {
        collapseWhitespace: true, // 折叠空白
        removeComments: true, // 移除注释
        removeAttributeQuotes: true // 移除属性的引号
      }
    })
  ],
  ···  
};

可以在新建 src/index.html 作为模版,执行打包命令:

> npm run dist

> testwebpack@1.0.0 dist /Users/TJing/Documents/webProjects/testWebpack
> npx webpack --config webpack.product.config.js

Hash: b6f4a880e0371e2f8ad3

最终打包生成 main.b6f4a880e0371e2f8ad3.css、main.b6f4a880e0371e2f8ad3.js 和 index.html,此时HTML自动注入了CSS和JS:

<!DOCTYPE html>
<html lang=en>
<head>
  <meta charset=UTF-8>
  <title></title>
  <link href=main.b6f4a880e0371e2f8ad3.css rel=stylesheet>
</head>
<body>
<script type=text/javascript data-original=main.b6f4a880e0371e2f8ad3.js></script>
</body>
</html>

4.3 清理 dist 打包目录

每次编译后 /dist 文件夹都会保存生成的文件,特别是配置文件民Hash值后。通常推荐在每次构建前清理 /dist 文件夹。

clean-webpack-plugin 是一个比较普及的管理插件,安装和配置如下:

npm i -D clean-webpack-plugin
const CleanWebpackPlugin = require('clean-webpack-plugin');

module.exports = {
  mode: 'production',
···  
  plugins: [
+   new CleanWebpackPlugin()
      ...
  ],
···  
};

五、开发辅助

5.1 Sourse map 源码追踪

webpack会把三个源文件(a.js, b.js 和 c.js)打包到一个 bundle(bundle.js)中,在开发环境下很难 debug 到原始的开发代码,这时就要使用 source map 解决开发代码与实际运行代码不一致的问题。

在 devtool 设置 source map 增强调试过程,source map 类型传送门

// webpack.config.js

  module.exports = {
    entry: './src/index.js',
    // ...
    devtool: 'inline-source-map',
  };

还可以使用 SourceMapDevToolPlugin 插件替代 devtool 选项进行更细粒度的配置;

对于样式调试,webpack 会把 css、sass 模块打包到 style 模块中,css-loader 和 sass-loader 都可以通过该 options 设置启用 sourcemap。

// webpack.config.js
    rules: [ // 规则数组,修改模块的创建方式
      {
        test: /\.(sc|c|sa)ss$/,  // 正则表达式,处理scss sass css
        // use: ['style-loader', 'css-loader','sass-loader']
        use: ['style-loader',
          {
          loader: 'css-loader',
          options: {
            sourceMap: true}},
          {
          loader: 'sass-loader',
          options: {
            sourceMap: true}}]
      }
    ]

5.2 --watch 自动编译

每次修改完毕后都手动编译太麻烦,最简单解决的办法就是启动 watch 监控更新自动编译。

可以在启动编译时增加 --watch 开启

"scripts": {
    "watch": "npx webpack --watch --config webpack.dev.js",
    ···
  },

增加 --watch 后,每次修改后手动刷新浏览器页面,就可以看到更新内容。但如何能不刷新页面,自动更新变化呢?

5.3 webpack-dev-server 热更新

使用 webpack-dev-server ,实际就是创建一个简单的 web 服务器,能够实时重新加载(live reloading)。

npm install --save-dev webpack-dev-server

配置只在开发环境,,除了要配置 devServer 属性,还需要在 plugins 中增加两个插件:

const webpack = require('webpack');

module.exports = {
  mode: 'development',
  devtool: 'inline-source-map', // js 的 sourcemap
  devServer: {
    contentBase: './dist',
    hot: true,
    port: 9000
  },
  plugins: [
    new webpack.NamedModulesPlugin(),  // 更容易查看(patch)的依赖
    new webpack.HotModuleReplacementPlugin()  // 替换插件
  ]
 ··· 

执行以下命令就会在 http://localhost:9000/ 中看到主页面。注意到,使用 webpack-dev-server 编译后的文件直接在内存中,不会输出到dist目录,但可以使用dist目录中的文件。

npx webpack-dev-server --config webpack.dev.js

dev-server官网 中其他的配置:

devServer: {
  clientLogLevel: 'warning', // 可能的值有 none, error, warning 或者 info(默认值)
  hot: true,  // 启用 webpack 的模块热替换特性, 这个需要配合: webpack.HotModuleReplacementPlugin插件
  contentBase:  path.join(__dirname, "dist"), // 告诉服务器从哪里提供内容, 默认情况下,将使用当前工作目录作为提供内容的目录
  compress: true, // 一切服务都启用gzip 压缩
  host: '0.0.0.0', // 指定使用一个 host。默认是 localhost。如果你希望服务器外部可访问 0.0.0.0
  port: 8080, // 端口
  open: true, // 是否打开浏览器
  overlay: {  // 出现错误或者警告的时候,是否覆盖页面线上错误消息。
    warnings: true,
    errors: true
  },
  publicPath: '/', // 此路径下的打包文件可在浏览器中访问。
  proxy: {  // 设置代理
    "/api": {  // 访问api开头的请求,会跳转到  下面的target配置
      target: "http://192.168.0.102:8080",
      pathRewrite: {"^/api" : "/mockjsdata/5/api"}
    }
  },
  quiet: true, // necessary for FriendlyErrorsPlugin. 启用 quiet 后,除了初始启动信息之外的任何内容都不会被打印到控制台。这也意味着来自 webpack 的错误或警告在控制台不可见。
  watchOptions: { // 监视文件相关的控制选项
    poll: true,   // webpack 使用文件系统(file system)获取文件改动的通知。在某些情况下,不会正常工作。例如,当使用 Network File System (NFS) 时。Vagrant 也有很多问题。在这些情况下,请使用轮询. poll: true。当然 poll也可以设置成毫秒数,比如:  poll: 1000
    ignored: /node_modules/, // 忽略监控的文件夹,正则
    aggregateTimeout: 300 // 默认值,当第一个文件更改,会在重新构建前增加延迟
  }
}

如果想要重温Webpack的基础配置可以从官网指南入手,想要了解更复杂灵活的功能可以添加各种插件,如果有兴趣自己写一个loader,送个666给你~

architecture-building-exterior-1029599.jpg

查看原文

赞 8 收藏 8 评论 0

TiJay 赞了文章 · 2019-10-18

小程序技能进阶回忆录 - 也许你并不需要小程序框架

你也许并不需要小程序框架。

市面上不停的会有大的公司推出自己的小程序的研发库 / 框架,功能十分强大,也为小程序的开发带来了便利。但在一些积极的反馈中,我们也看到不一样的声音:

  • @白小虫:又一个轮子。。
  • @jsweber:小程序不用任何框架,开发体验也不错啊,本身就借鉴了 vue 和 react 的思想。
  • @月月木子:现在中上流公司的前端都很热衷于造自己的轮子或者给别人的轮子换皮然后说是自己的轮子,疯狂垒高自身的技术壁垒,即有了给领导吹牛的资本,让新来的人肯定属于不熟练工,又可以给自身带来安全感,不知道到底是好是坏。
  • @redbuck:轮子越造越多,我寻思下一个小程序要不转回原生算了。
  • @fantasy525:感觉一次编码全端支持没多大必要,支持的越多就可能会出越多的bug,我们开发时会很难受,本来只解决一端的bug,多端我们又要解决其他端,还不如各司其职好。
  • @肉很多:学不动了呀。。。。😠
上列评论从掘金用户评论中挑选。

这几天公司校招,面了一些在校生,其中有两位同学让人印象深刻:

一、同学 A 面试接近尾声突然问起,你们被美团收购后,是不是都要必须使用 mpvue(美团早年推出的小程序框架)?当我回答不是后,同学 A 长舒一口气:那就好。他解释道:更偏向用原生去写小程序,因为微信团队更新节奏比较快,框架经常跟不上微信的节奏,导致新特性无法在项目中使用。

二、同学 B 在简历中写道精通 jQuery,我在想这年头了,jQ 在简历中出现的越来越少了,故意抓着这个问了下,果然比较了解。他说道:经常用 jQ 做一些网页 demo,因为上手比较容易,直接引入一个cdn js就行,都不用装 node 包。

em... 好像说的都挺在理。

在摩拜单车内部,我们封装了基于微信小程序原生语法进行扩展、对原生微信 API 支持友好的小程序基础库 - Mozzy。注意:其定位是基础库,而不是框架。只要在原生语法的小程序项目里引入一个 js 文件就可以使用,即便是开发到一半的小程序也可以快速引入。

记得刚做完这个项目在公司内部分享时,说到未来的愿景时最后一句是:

也许有一天,当使用了 Mozzy 开发的小程序,删除 mozzy.js 后,发现功能竟然一切正常。

相信早些年用 jQ 做项目的时候很多同学都脑洞过,要是浏览器原生支持 jQ 的 api 多好,或者干脆浏览器直接集成 jQ,就不用在每个项目的 html 里都引入一段 jQ 代码了,毕竟 jQ 在当年几乎是网页开发必备基础库。

随着微信官方 api 的更新,Mozzy 的很多实现都有了官方支持。也许当时分享的未来愿景已经来了,最近要开启删(改造) Mozzy 行动,换种方式存在于千万行代码中。

拥抱变化。

接下来的一系列文章里,会记录下 Mozzy 甚至是整个摩拜单车小程序研发细节和心路历程,可称为小程序技能进阶回忆录

主要内容目录如下,大部分已经写完,会不定期进行更新:

  • 小程序技能进阶回忆录 - 在缺少组件化的日子里
  • 小程序技能进阶回忆录 - 自主实现数据侦听器和计算器
  • 小程序技能进阶回忆录 - 自主实现拦截器
  • 小程序技能进阶回忆录 - globalData 的那些事儿
  • 小程序技能进阶回忆录 - 什么时候执行 onLoad
  • 小程序技能进阶回忆录 - 增强型的 wx.navigateBack

当然,不排除标题修改为“标题党”形式糊弄人。正如此篇一样。

查看原文

赞 8 收藏 3 评论 0

TiJay 关注了用户 · 2019-09-27

LeonWuV @wxl1555

写了很多代码,也扔了不少,现在在跑的也没有多少。
github:https://github.com/LeonWuV
个人站点:https://xiaolongwu.cn
csdn:https://blog.csdn.net/wxl1555
博客园:https://www.cnblogs.com/wuxia...
掘金:https://juejin.im/user/593693...

关注 4

TiJay 赞了文章 · 2019-08-01

Vue,React,微信小程序,快应用,TS 和 Koa 一把梭

图片描述

前言

前端生态越来越繁华,随着资本寒冬的来临,对前端招聘要求也变高了;
本文将从项目出发由浅入深做一个Vue,React,微信小程序,快应用,TS和 Koa的知识大串联;
相当于一篇文章搞定前端目前主流技术栈。

1.源码(持续更新)

话不多说,源码地址:Vue,React,微信小程序,快应用,TS 和 Koa 地址,欢迎 star
项目目录:
图片描述

2.vue 篇

2.1 vue-demo

2.1.1效果图

图片描述

Vue,React,微信小程序,快应用,TS 和 Koa 地址,欢迎 star

2.1.2.技术栈

vue+vue-router+vuex+axios+element-UI+iconfont(阿里)

2.1.3.适配方案

左侧固定宽度,右侧自适应
左侧导航和右侧导航分别配置滚动条

2.1.4.技能点分析

技能点对应api
常用指令@(v-on)绑定事件, v-if/v-show是否创建/和是否显示,v-for循环
生命周期8个生命周期beforeCreate,created,beforeMount,mounted,beforeUpdate,updated,beforeDestroy和destroy
观察计算computed和watch
data属性定义变量,同样变量使用必须先定义
组件注册components局部注册,Vue.component()全局注册
组件通讯子传父:this.$emit,父传子:props,平级组件:vuex或路由传参
插件注册Vue.use()注册插件,如Vue.use(element)是调用element内部的install方法
路由注册vue-router:Vue.use(router)也是调用内部的install方法,挂载到vue实例中生成route和router属性
路由模式mode属性可以设置history和hash
子路由children:[]可以配置子路由
路由钩子router.beforeEach(实现导航钩子守卫)和router.afterEach
vuex4个属性,state,getters, actions(异步获取数据)和mutations(同步获取数据)
vuex4个辅助函数,mapState,mapGetters, mapActions和mapMutations,就是辅助处理commit或distapch方法
axios拦截器,interceptors.request请求拦截器,interceptors.response响应拦截器
axiosbaseUrl配置公共请求路径,必须符合http标准的链接,否则设置无效
axios请求方法,get,post,put,delete等
axios跨域,withCredentials: true,需要后端支持
csssass,对应嵌套不超过三层,滚动条样式设置,文本两行超出build问题
iconfont阿里字体图标,可以自定义icon

2.1.5.那么问题来了?

computed和watch的区别? 解析
路由传参的方法? 解析
vue.use,vue.install,mixins方法区别? 解析
history和hash区别及实现原理? 区别解析原理解析vue-router官网
使用history和hash模式部署服务器有什么问题?问题解析
vuex的辅助函数和基本属性使用的区别?vuex官网
axios原理?axios源码
简单实现一个vue+vue-router+vuex的框架?

2.2 vue-mobile-demo

2.2.1 效果图

图片描述

2.2.2技术栈

vue+vue-router+vuex+vant+rem+sass+iconfont(阿里)
vant:有赞的电商mobile插件

2.2.3适配方案

rem

2.2.4技能点分析

iconfont的使用:官网配置icon,导出图标,引入assets目录下
vant使用:详见vant官网
全局配置rem:在index.html文件配置
全局配置sass函数和mixin:在build/utils下面的scss的options属性配置static目录下面的函数和混入

2.2.5那么问题来了

vue-cli生成的项目src下面的assets和根路径下面的static目录的区别?解析

3. react 篇

3.1 react-mobile篇

3.1.1效果图

图片描述
Vue,React,微信小程序,快应用,TS 和 Koa 地址,欢迎 star

3.1.2技术栈

react + react-router-v4 + redux +ant-design-mobile+iconfont
react-router-v4:路由4.x版本
redux:状态管理
ant-design-mobile:UI组件
iconfont:字体icon

3.1.3适配方案

rem适配

3.1.4技能点分析

技能点对应的api
3种定义react组件方法1.函数式定义的无状态组件; 2.es5原生方式React.createClass定义的组件; 3.es6形式的extends React.Component定义的组件
JSXreact是基于jSX语法
react16之前生命周期实例化(6个):constructor,getDefaultProps,getInitialState,componentWillMount,render,componentDidMount
react16生命周期实例化(4个):constructor,getDerivedStateFromProps,componentWillMount,render,componentDidMount,componentWillUpdata,render,componentDidUpdate, componentWillUnmount
生命周期更新(5个) componentWillReceivePorps,shouldComponentUpdate,
生命周期销毁:componentWillUnmout
react-dom提供render方法
react-router 4.x组成react-router(核心路由和函数) , react-router-dom(API) , react-router-native( React Native 应用使用的API)
react-router 4.x的APIrouter(只能有一个) , route(匹配路由渲染UI) , history, link(跳转) , navlink(特定的link,会带样式) , switch(匹配第一个路由) , redirect(重定向) , withRouter(组件,可传入history,location,match 三个对象)
react-router 3.x组成就是react-router
react-router 3.x的APIrouter , route , history(push和replace方法) , indexRedirect(默认加载) , indexRedirect(默认重定向) , link(跳转) , 路由钩子(onEnter进入,onLeave离开)4.x已经去掉
historyreact-router有三种模式:
1.browserHistory(需要后台支持);
2.hashHistory(有'#');
3.createMemoryHistory
redux单向数据流 , action(通过dispatch改变state值) , reducer(根据 action 更新 state) , store(联系action和reducer)
react-redux1.连接react-router和redux,将组件分为两类:UI组件和容器组件(管理数据和逻辑) ,
2.connect由UI组件生成容器组件 ,
3.provider让容器组件拿到state ,
4.mapStateToProps:外部state对象和UI组件的props映射关系,
5.mapDispatchToProps:是connect第二个参数, UI 组件的参数到store.dispatch方法的映射
react-loadable代码分割,相当于vue-router中的路由懒加载
classNames动态css的类

3.2 react-pc-template篇

3.2.1效果图

图片描述
Vue,React,微信小程序,快应用,TS 和 Koa 地址,欢迎 star

3.2.2技术栈

dva+umi+ant-design-pro
dva:可拔插的react应用框架,基于react和redux
mui:集成react的router和redux
ant-design-pro:基于react和ant-pc的中后台解决方案

3.2.3适配方案

左侧固定宽度,右侧自适应
右侧导航分别配置滚动条.控制整个page

3.2.4技能点分析

技能点对应api
路由基于umi,里面有push,replace,go等方法
状态管理dva里面的redux的封装,属性有state,effects,reducers
组件传值父子:props,平级redux或umi的router
model项目的model和dom是通过@connect()连接并将部分属性添加到props里
登陆登陆是通过在入口js里面做路由判断

4.微信小程序篇

4.1小程序demo

4.1.1效果

图片描述

Vue,React,微信小程序,快应用,TS 和 Koa 地址,欢迎 star

4.1.2技术栈

weui+tabbar+分包+iconfont+自定义顶部导航+组件传值+wx.request封装
weui:Tencent推出的小程序UI

4.1.3适配方案

rpx:微信小程序的单位

4.1.4技能点分析

技能点对应api
view布局容器,是块级元素
text文字容器,行内元素
image图片容器,块级元素
常用指令bindtap绑定事件, wx:if/wx:show是否创建/和是否显示,wx:for循环
生命周期1应用生命周期(app.js里):launch,show,hide
生命周期2页面生命周期(page里):load,show,ready,hide,unload
生命周期3组件周期(component里):created,attached,moved,detached
wx.requestajax请求
背景音乐wx.getBackgroundAudioManager
音频wx.createAudioContext
图片wx.chooseImage
文件wx.getFileInfo
路由在app.json里面pages属性定义pages目录下面的文件
路由切换wx.navigateTo,wx.navigateBack, wx.redirectTo,wx.switchTab,wx.reLaunch
分包在app.json里面subPackages属性定义分包路由
weui组件weui官网
原生组件微信原生组件
业务组件在json文件usingComponents注册
组件通讯定义globalData,storage和路由

4.1.5那么问题来了

小程序的生命周期执行顺序?page和应用生命周期component生命周期解释
几种路由切换有什么不同?路由介绍
小程序怎么实现watch监听数据变化?实现watch

4.1.6小程序框架

wepy官网
基于wepy的商城项目
mpVue
基于mpVue的项目

分析:这两个框架都是通过预编译将对应风格的格式转化成小程序格式

5.快应用篇

5.1 快应用模板

5.1.1技能点分析

技能点对应api
布局基于弹性布局
指令for:循环,if、show
生命周期页面的生命周期:onInit、onReady、onShow、onHide、onDestroy、onBackPress、onMenuPress
app生命周期onCreate、onDestroy
事件$on、$off、$emit、$emitElement
路由配置manifest文件的router属性配置
路由跳转router.page
组件通讯父子组件:$emit,props,兄弟组件:通过 Publish/Subscribe 模型
原生组件list,map,tabs和canvas
消息机制websocket使用

6.TS篇

6.1 TS前言

为什么会有TS? 大家有没想过这个问题,原因是JS是弱类型编程语言,也就是申明变量类型可以任意变换。所以这个时候TS出现了。
TS 是 JS 的超集,也相当于预处理器,本文通过一个template项目来让你快速上手TS。

6.2效果图

图片描述
Vue,React,微信小程序,快应用,TS 和 Koa 地址,欢迎 star

6.3技术栈

1.vue
2.vue-cli3
3.vue-router
4.vuex
5.typescript
6.iconfont

6.4核心插件

技能点对应的api
vue-class-component是vue官方提供的,暴露了vue和component实例
vue-property-decorator是社区提供
深度依赖vue-class-component拓展出了很多操作符@Component @Prop @Emit @Watch @Inject
可以说是 vue class component 的一个超集,正常开发的时候 你只需要使用 vue property decorator 中提供的操作符即可

vue-property-decorator暴露的API

API作用
@Component注册组件
get类似vue的computed
@Prop,@Emit组件传值
@Watch监听值变化
@Privde,@Inject对应privde和inject
高阶组件用法,作用是多级父组件传值给子
@Model类似vue的model

6.5 TS语法

数据类型any(任意类型);
number;
string,
boolean;
数组:number[]或new Array(项的数据类型相同);
void返回值类型;
null;
undefined;
never(从不出现值);
元祖(比数组强大,项的类型可以不同);
接口:interface关键字;
对象:类似JS的object;
函数:function声明;
类:class关键字,包括字段,构造函数和方法
变量声明let [变量名] : [类型] = 值, 必须指定类型
声明array,let arr: any[] = [1, 2]
运算符,条件语句,循环同JS
函数声明同JS,形参必须指定类型,因为形参也是变量
联合类型通过竖线声明一组值为多种类型
命名空间namespace关键字
模块import和export
访问控制符public,private(只能被其定义所在的类访问)和protected(可以被其自身以及其子类和父类访问)
默认public,是不是有点Java的味道

6.6问题来了

1.怎么在项目手动配置ts?
vue+ts项目配置

2.接口和类的区别?
接口只声明成员方法,不做实现 ,class通过implements 来实现接口
ts中接口和类的区别

3.接口和对象的区别?
接口是公共属性或方法的集合,可以通过类去实现;
对象只是键值对的实例

4.类class和函数的区别?
类是关键字class,函数是function
类可以实现接口

5.接口实现继承方法?

interface Person { 
   age:number 
} 
 
interface Musician extends Person { 
   instrument:string 
} 
 
var drummer = <Musician>{}; 
drummer.age = 27 
drummer.instrument = "Drums" 
console.log("年龄:  "+drummer.age)
console.log("喜欢的乐器:  "+drummer.instrument)

7. koa 篇

7.1 koa前言

node.js的出现前端已经可以用js一把梭,从前端写到后台。
本文从后台利用node的框架koa+mongodb实现数据的增删改查和注册接口,前端利用umi + dva +ant-design-pro来实现数据渲染。实现一个小全栈project,就是so-easy

7.2效果图

图片描述
Vue,React,微信小程序,快应用,TS 和 Koa 地址,欢迎 star

7.3技术栈

koa:node框架
koa-bodyparser:解析body的中间件
koa-router :解析router的中间件
mongoose :基于mongdodb的数据库框架,操作数据
nodemon:后台服务启动热更新

7.4项目目录

├── app // 主项目目录
│ ├── controllrts // 控制器目录(数据处理)
│ │ └── ... // 各个表对应的控制器
│ ├── middleware // 中间件目录
│ │ └── resFormat.js // 格式化返回值
│ ├── models // 表目录(数据模型)
│ │ ├── course.js // 课程表
│ │ └── user.js // 用户表
│ └── utils // 工具库
│ │ ├── formatDate.js // 时间格式化
│ │ └── passport.js // 用户密码加密和验证工具
├── db-template // 数据库导出的 json 文件
├── routes // 路由目录
│ └── api // 接口目录
│ │ ├── course_router.js // 课程相关接口
│ │ └── user_router.js // 用户相关接口
├── app.js // 项目入口
└── config.js // 基础配置信息

7.5项目启动步骤

1.git clone
2.安装mongodb:http://www.runoob.com/mongodb...
3.安装 Robomongo 或Robo 3T是mongodb可视化操作工具 (可选)
4.启动
mongod (启动 mongodb)
打开Robomongo 或Robo
cd koa-template
npm i
npm run start
cd react-template
npm i
npm run start

注意:
mongodb启动默认端口号是27017,启动看是否被占用
后端项目端口号是3000,可以在koa-template/config.js里面修改

7.6 koa的主要API

API作用
new koa()得到koa实例
usekoa的属性,添加中间件
context将 node 的 request 和 response 对象封装到单个对象中,每个请求都将创建一个 Context,通过ctx访问暴露的方法
ctx方法request:请求主体;
response:响应主体;
ctx.cookies.get:获取cookie;
ctx.throw:抛出异常
request属性header:请求头;
method:方法;
url:请求url;
originalUrl请求原始URL;
href:完整URL;
hostname:主机名;
type:请求头类型;
response属性header:响应头;
status:状态,未设置默认为200或204;
body:响应主体,string(提示信息) Buffer Stream(流) Object Array JSON-字符串化ull 无内容响应;
get:获取响应头字段;
set:设置响应头;
append:添加响应头;
type:响应类型;
lastModified:返回为 Date, 如果存在;
etag:设置缓存

7.7 koa-router主要API

API作用
getget方法
postpost方法
patchpatch方法
deletedelete方法
prefix配置公共路由路径
use将路由分层,同一个实例router中可以配置成不同模块
ctx.params获取动态路由参数
fs分割文件

7.8 mongoose主要API

API作用
Schema数据模式,表结构的定义;每个schema会映射到mongodb中的一个collection,它不具备操作数据库的能力
modelschema生成的模型,可以对数据库的操作

model的操作database方法

API方法
create/save创建
remove移除
delete删除一个
deleteMany删除多个
find查找
findById通过id查找
findOne找到一个
count匹配文档数量
update更新
updateOne更新一个
updateMany更新多个
findOneAndUpdate找到一个并更新
findByIdAndUpdate通过id查找并更新
findOneAndRemove找到一个并移除
replaceOne替换一个
watch监听变化

query查询API

API作用
where指定一个 path
equals等于
or
nor不是
gt大于
lt小于
size大小
exists存在
within在什么之内

注:Query是通过Model.find()来实例化
aggregate(聚合)API

API作用
append追加
addFields追加文件
limit限制大小
sort排序

注:aggregate=Model.aggregate()

更多详细API,请戳

查看原文

赞 117 收藏 88 评论 9

TiJay 赞了文章 · 2019-08-01

CSS实现8种炫酷按钮

在各种UI组件库大行其道的今天,大家已经很少自己用CSS去实现一些效果了,久而久之CSS的水平也越来越退步,所以有空还是得练练。今天给大家分享8种炫酷按钮的CSS实现。

1. 3D按钮1

3D Button 1 Gif

现在的主流是扁平化的设计,拟物化的设计比较少见了,所以我们仅从技术角度去分析如何实现这个3D按钮(文中只列出各种实现的关键代码,完整代码请参考CodePen)。

该按钮的立体效果主要由按钮多出的左、下两个侧面衬托出来,我们可以使用box-shadow模拟出这两个侧面:

HTML:

<button class="button-3d-1">3D Button 1</button>

CSS:

.button-3d-1{
  position: relative;
  background: orangered;
  border: none;
  color: white;
  padding: 15px 24px;
  font-size: 1.4rem;
  outline: none;
  box-shadow: -6px 6px 0 hsl(16, 100%, 30%);
}

效果:

just box shadow

加了box-shadow之后整体形状有点像了,但是立方体的左上和右下确缺了一块。通过观察我们发现,缺的这两块是三角形的,所以如果我们能构造两个三角形补到这两个角上就行了。用CSS画三角形对大家来说应该是基本操作了,如果还有同学不知道,下面的动画很好的解释了原理(代码参考 Triangle):

triangle animation

我们使用::before::after伪元素来分别绘制左上、右下的两个三角形,并通过绝对定位将它们固定到两个角落:

CSS:

.button-3d-1::before {
  content: "";
  display: block;
  width: 0;
  height: 0;
  position: absolute;
  top: 0;
  left: -6px;
  border: 6px solid transparent;
  border-right: 6px solid hsl(16, 100%, 30%);
  border-left-width: 0px;
}

.button-3d-1::after {
  content: "";
  display: block;
  width: 0;
  height: 0;
  position: absolute;
  bottom: -6px;
  right: 0;
  border: 6px solid transparent;
  border-top: 6px solid hsl(16, 100%, 30%);
  border-bottom-width: 0px;
}

接下来,我们需要实现点击时按钮被按下的效果,思路是点击时将按钮正面向左下角移动,同时减少box-shadow的偏移量以达到按钮底部固定不动的效果:

CSS:

.button-3d-1:active {
  background: hsl(16, 100%, 40%);
  top: 3px;
  left: -3px;
  box-shadow: -3px 3px 0 hsl(16, 100%, 30%);
}

最后,我们需要重新计算左上、右下两个三角形的尺寸和位置,以适应按下后上下缺口的变小:

CSS:

.button-3d-1:active::before {
  border: 3px solid transparent;
  border-right: 3px solid hsl(16, 100%, 30%);
  border-left-width: 0px;
  left: -3px;
}

.button-3d-1:active::after {
  border: 3px solid transparent;
  border-top: 3px solid hsl(16, 100%, 30%);
  border-bottom-width: 0px;
  bottom: -3px;
}

最终效果:CodePen 3D Button 1

2. 3D按钮2

3D Button 2 Gif

对于这种圆柱形的按钮,思路和上面矩形3D的按钮类似,也是通过box-shadow构造侧面呈现立体感。为了使效果更加真实,侧面的颜色呈现渐变效果,越往下颜色越深,因此我们可以通过叠加多层box-shadow来实现:

HTML:

<button class="button-3d-2">Circle!</button>

CSS:

.button-3d-2{
  position: relative;
  background: #ecd300;
  background: radial-gradient(hsl(54, 100%, 50%), hsl(54, 100%, 40%));
  font-size: 1.4rem;
  text-shadow: 0 -1px 0 #c3af07;
  color: white;
  border: 1px solid hsl(54, 100%, 20%);
  border-radius: 100%;
  height: 120px;
  width: 120px;
  z-index: 4;
  outline: none;

  box-shadow: inset 0 1px 0 hsl(54, 100%, 50%),
              0 2px 0 hsl(54, 100%, 20%),
              0 3px 0 hsl(54, 100%, 18%),
              0 4px 0 hsl(54, 100%, 16%),
              0 5px 0 hsl(54, 100%, 14%),
              0 6px 0 hsl(54, 100%, 12%),
              0 7px 0 hsl(54, 100%, 10%),
              0 8px 0 hsl(54, 100%, 8%),
              0 9px 0 hsl(54, 100%, 6%);
}

当点击按钮的时候,通过下移按钮和减少box-shadow的层数来实现按钮被按下的效果:

CSS:

.button-3d-2:active{
  ...
  top: 2px;
  box-shadow: inset 0 1px 0 hsl(54, 100%, 50%),
              0 2px 0 hsl(54, 100%, 16%),
              0 3px 0 hsl(54, 100%, 14%),
              0 4px 0 hsl(54, 100%, 12%),
              0 5px 0 hsl(54, 100%, 10%),
              0 6px 0 hsl(54, 100%, 8%),
              0 7px 0 hsl(54, 100%, 6%);
}

最终效果:CodePen 3D Button 2

3. 渐变按钮1

Gradient Button 1

提到渐变色大家一般都会想到background-image属性可以设置为linear-gradient以呈现渐变色的背景,事实上linear-gradient的类型属于<image>的一种,所以凡是可以使用图片的属性都可以用linear-gradient代替,包括border-image。实现这个按钮的关键在于实现一个渐变色和边框,而且当鼠标悬浮的时候边框和背景融为一体。

首先,我们实现渐变色的边框。

HTML:

<button class="gradient-button-1">Gradient Button 1</button>

CSS:

.gradient-button-1{
  position: relative;
  z-index: 1;
  display: inline-block;
  padding: 20px 40px;
  font-size: 1.4rem;
  box-sizing: border-box;
  background-color: #e7eef1;
  color: orangered;
  border:solid 10px transparent;
  border-image: linear-gradient(to top right, orangered, yellow);
}

效果:

just border image

很奇怪,只有四个顶点有图像。这是因为border-image-slice默认为100%,所以导致四条边线上并没有分配背景图像。border-image-slice的用法参考 MDN,简而言之就是用四条(上下、左右各两条平行线,想象一下九宫格火锅。。)将图片切割成9块,在border的对应区域显示对应的图像切片,而border-image-slice的值就是那四条线的偏移量。这下大家应该能理解偏移量为100%的时候只有四个顶点上才有图片了吧。

所以我们需要调整border-image-slice的值,鉴于border-image-sourcelinear-gradient,我们需要将border-image-slice的值设置为1(表示四条线的偏移量都为1px)才能显示连续的渐变色背景:

CSS:

.gradient-button-1{
  ...
  border-image-slice: 1;
}

最后,我们只需要在鼠标悬浮的时候用渐变色填充按钮内部就行了,此处有两种实现,用 background-image 或者 将border-image-slice 设置为 fill (表示填充border中间的区域):

CSS:

.gradient-button-1:hover{
  color: white;
  background-image: linear-gradient(to top right, orangered, yellow);

  /* 或者 */

  border-image-slice: 1 fill;
}

最终效果:CodePen Gradient Button 1

4. 渐变按钮2

Gradient Button 2

与上一种按钮类似,只不过颜色是逐渐变透明,实现也类似:

HTML:

<button class="gradient-button-2">Gradient Button 2</button>

CSS:

.gradient-button-2{
  ...
  border:solid 4px transparent;
  border-image: linear-gradient(to right, orangered, transparent);
  border-image-slice: 1;
}

.gradient-button-2:hover{
  color: white;
  background-image: linear-gradient(to right, orangered, transparent);
  border-right: none;
}
注意点:当hover的时候需要设置 border-right: none,否则右侧边框无法呈现消失的状态。

最终效果:CodePen Gradient Button 2

5. 动画按钮1

Animted Button 1

给按钮加上一个动态背景的思路是:先找一个可以repeat的背景图(可以去 siteorigin 生成),然后使用keyframe自定义一段动画,当鼠标悬浮在按钮上的时候运行该动画:

HTML:

<button class="animated-button-1">Animated Button 1</button>

CSS:

.animated-button-1{
  position: relative;
  display: inline-block;
  padding:  20px 40px;
  font-size: 1.4rem;
  background-color: #00b3b4;
  background-image: url("wave.png");
  background-size: 46px 26px;
  border: 1px solid #555;
  color: white;
  transition: all ease 0.3s;
}

.animated-button-1:hover{
  animation: waving 2s linear infinite;
}

@keyframes waving{
  from{
    background-position: 0 0;
  }
  to{
    background-position: 46px 0;
  }
}
注意点:background-position 水平方向的值需要等于背景图片的宽度或其整数倍,这样动画循环播放的时候首尾才能平稳过渡。

最终效果:CodePen Animted Button 1

6. 动画按钮2

Animated Button 2

该按钮的实现思路是:用 ::after 伪元素创建右侧的箭头,使用绝对定位固定在按钮右侧,静止状态下通过设置opacity: 0隐藏,当鼠标悬浮时,增大按钮的padding-right,同时增加箭头的不透明度,并将其位置往左移动:

HTML:

<button class="animated-button-2">Animated Button 2</button>

CSS:

.animated-button-2{
  position: relative;
  padding:  20px 40px;
  font-size: 1.4rem;
  background-color: #00b3b4;
  background-size: 46px 26px;
  border: 1px solid #555;  
  color: white;
  transition: all ease 0.3s;
}

.animated-button-2::after{
  position: absolute;
  top: 50%;
  right: 0.6em;
  transform: translateY(-50%);
  content: "»";
  font-size: 1.2em;
  transition: all ease 0.3s;
  opacity: 0;
}

.animated-button-2:hover{
  padding: 20px 60px 20px 20px;
}

.animated-button-2:hover::after{
  right: 1.2em;
  opacity: 1;
}

最终效果:CodePen Animated Button 2

7. 开关按钮1

Switch Button 1

这算是一个挺常见的开关按钮,它的实现思路是:

  1. 通过一个checkbox记录开关的状态,并隐藏该checkbox;
  2. 使用一个 <label> 作为整个按钮容器,并通过 for 属性与checkbox关联,这样点击按钮的任何地方都能改变checkbox的状态;
  3. 使用一个 <span> 作为按钮可视的部分,并作为 checkbox 的相邻元素,这样通过 checkbox的伪类选择器 :checked 和相邻选择器 + 选中 <span>并显示不同状态下的内容。

HTML:

<label for="toggle1" class="toggle1">
  <input type="checkbox" id="toggle1" class="toggle1-input">
  <span class="toggle1-button"></span>
</label>

CSS:

.toggle1{
  vertical-align: top;
  width: 80px;
  display: block;
  margin: 100px auto;
}

.toggle1-input{
  display: none;
}

.toggle1-button{
  position: relative;
  display: inline-block;
  font-size: 1rem;
  line-height: 20px;
  text-transform: uppercase;
  background-color: #f2395a;
  border: 1px solid #f2395a;
  color: white;
  width: 100%;
  height: 30px;
  transition: all 0.3s ease;
  cursor: pointer;
}


.toggle1-button::before{
  position: absolute;
  top: 5px;
  left: 38px;
  content: "off";
  display: inline-block;
  height: 20px;
  padding: 0 3px;
  background: white;
  color: #f2395a;
  transition: all 0.3s ease;
}

.toggle1-input:checked + .toggle1-button{
 background: #00b3b4; 
 border-color: #00b3b4;
}

.toggle1-input:checked + .toggle1-button::before{
  left: 5px;
  content: 'on';
  color: #00b3b4;
}
注意点:<label>for 属性的作用;:checked 伪类的使用;+ 相邻选择器的使用。

最终效果:CodePen Switch Button 1

8. 开关按钮2

Switch Button 2

与开关按钮1类似,动画效果上更简单,只要切换颜色就行了:

HTML:

<label for="toggle2" class="toggle2">
  <input type="checkbox" id="toggle2" class="toggle2-input">
  <span class="toggle2-button">Click to activate</span>
</label>

CSS:

.toggle2{
  font-size: 0.8rem;
  display: inline-block;
  vertical-align: top;
  margin: 0 15px 0 0;
}

.toggle2-input{
  display: none;
}

.toggle2-button{
  position: relative;
  display: inline-block;
  line-height: 20px;
  text-transform: uppercase;
  background: white;
  color: #aaa;
  border: 1px solid #ccc;
  padding: 5px 10px 5px 30px;
  transition: all 0.3s ease;
  cursor: pointer;
}

.toggle2-button::before{
  position: absolute;
  top: 10px;
  left: 10px;
  display: inline-block;
  width: 10px;
  height: 10px;
  background: #ccc;
  content: "";
  transition: all 0.3s ease;
  border-radius: 50%;
}

.toggle2-input:checked + .toggle2-button{
  background: #00b3b4;
  border-color: #00b3b4;
  color: white;
}

.toggle2-input:checked + .toggle2-button::before{
  background: white;
}

最终效果:CodePen Switch Button 2

本文参考:https://youtu.be/pmKyG3NBY_k
查看原文

赞 29 收藏 22 评论 0

TiJay 赞了文章 · 2019-05-30

详解vue组件三大核心概念

前言

本文主要介绍属性、事件和插槽这三个vue基础概念、使用方法及其容易被忽略的一些重要细节。如果你阅读别人写的组件,也可以从这三个部分展开,它们可以帮助你快速了解一个组件的所有功能。

clipboard.png

本文的代码请猛戳github博客,纸上得来终觉浅,大家动手多敲敲代码!

一、属性

1.自定义属性props

prop 定义了这个组件有哪些可配置的属性,组件的核心功能也都是它来确定的。写通用组件时,props 最好用对象的写法,这样可以针对每个属性设置类型、默认值或自定义校验属性的值,这点在组件开发中很重要,然而很多人却忽视,直接使用 props 的数组用法,这样的组件往往是不严谨的。

// 父组件
 <props name='属性'
           :type='type'
           :is-visible="false"
           :on-change="handlePropChange"
           :list=[22,33,44]
           title="属性Demo"
           class="test1"
           :class="['test2']"
           :style="{ marginTop: '20px' }" //注意:style 的优先级是要高于 style
           style="margin-top: 10px">
  </props>
// 子组件
  props: {
    name: String,
    type: {
   //从父级传入的 type,它的值必须是指定的 'success', 'warning', 'danger'中的一个,如果传入这三个以外的值,都会抛出一条警告
      validator: (value) => {
        return ['success', 'warning', 'danger'].includes(value)
      }
    },
    onChange: {
    //对于接收的数据,可以是各种数据类型,同样也可以传递一个函数
      type: Function,
      default: () => { }
    },
    isVisible: {
      type: Boolean,
      default: false
    },
    list: {
      type: Array,
      // 对象或数组默认值必须从一个工厂函数获取
      default: () => []
    }
  }

从上面的例中,可以得出props 可以显示定义一个或一个以上的数据,对于接收的数据,可以是各种数据类型,同样也可以传递一个函数。通过一般属性实现父向子通信;通过函数属性实现子向父通信

2.inheritAttrs

这是2.4.0 新增的一个API,默认情况下父作用域的不被认作 props 的特性绑定将会“回退”且作为普通的 HTML 特性应用在子组件的根元素上。可通过设置 inheritAttrs 为 false,这些默认行为将会被去掉。注意:这个选项不影响 class 和 style 绑定
上个例中,title属性没有在子组件中props中声明,就会默认挂在子组件的根元素上,如下图所示:

clipboard.png

3. data与props区别

  • 相同点

两者选项里都可以存放各种类型的数据,当行为操作改变时,所有行为操作所用到和模板所渲染的数据同时都会发生同步变化。

  • 不同点

data 被称之为动态数据,在各自实例中,在任何情况下,我们都可以随意改变它的数据类型和数据结构,不会被任何环境所影响。

props 被称之为静态数据,在各自实例中,一旦在初始化被定义好类型时,基于 Vue 是单向数据流,在数据传递时始终不能改变它的数据类型,而且不允许在子组件中直接操作 传递过来的props数据,而是需要通过别的手段,改变传递源中的数据。至于如何改变,我们接下去详细介绍:

4.单向数据流

这个概念出现在组件通信。props的数据都是通过父组件或者更高层级的组件数据或者字面量的方式进行传递的,不允许直接操作改变各自实例中的props数据,而是需要通过别的手段,改变传递源中的数据。那如果有时候我们想修改传递过来的prop,有哪些办法呢?

  • 方法1:过渡到 data 选项中

在子组件的 data 中拷贝一份 prop,data 是可以修改的

export default {
  props: {
    type: String
  },
  data () {
    return {
      currentType: this.type
    }
  }
}

在 data 选项里通过 currentType接收 props中type数据,相当于对 currentType= type进行一个赋值操作,不仅拿到了 currentType的数据,而且也可以改变 currentType数据。

  • 方法2:利用计算属性
export default {
  props: {
    type: String
  },
  computed: {
    normalizedType: function () {
      return this.type.toUpperCase();
    }
  }
}

以上两种方法虽可以在子组件间接修改props的值,但如果子组件想修改数据并且同步更新到父组件,却无济于事。在一些情况下,我们可能会需要对一个 prop 进行『双向绑定』,此时就推荐以下这两种方法:

  • 方法3:使用.sync
// 父组件
<template>
  <div class="hello">
    <div>
      <p>父组件msg:{{ msg }}</p>
      <p>父组件数组:{{ arr }}</p>
    </div>
    <button @click="show = true">打开model框</button>
    <br />
    <demo :show.sync="show" :msg.sync="msg" :arr="arr"></demo>
  </div>
</template>

<script>
import Demo from "./demo.vue";
export default {
  name: "Hello",
  components: {
    Demo
  },
  data() {
    return {
      show: false,
      msg: "模拟一个model框",
      arr: [1, 2, 3]
    };
  }
};
</script>
// 子组件
<template>
  <div v-if="show" class="border">
    <div>子组件msg:{{ msg }}</div>
    <div>子组件数组:{{ arr }}</div>
    <button @click="closeModel">关闭model框</button>
    <button @click="$emit('update:msg', '浪里行舟')">
      改变文字
    </button>
    <button @click="arr.push('前端工匠')">改变数组</button> 
  </div>
</template>
<script>
export default {
  props: {
    msg: {
      type: String
    },
    show: {
      type: Boolean
    },
    arr: {
      type: Array //在子组件中改变传递过来数组将会影响到父组件的状态
    }
  },
  methods: {
    closeModel() {
      this.$emit("update:show", false);
    }
  }
};

clipboard.png

父组件向子组件 props 里传递了 msg 和 show 两个值,都用了.sync 修饰符,进行双向绑定。
不过.sync 虽好,但也有限制,比如:

1)不能和表达式一起使用(如 v-bind:title.sync="doc.title + '!'" 是无效的);
2)不能用在字面量对象上(如 v-bind.sync="{ title: doc.title }" 是无法正常工作的)。

  • 方法4:将父组件中的数据包装成对象传递给子组件

这是因为在 JavaScript 中对象和数组是通过引用传入的,所以对于一个数组或对象类型的 prop 来说,在子组件中改变这个对象或数组本身将会影响到父组件的状态。比如上例中在子组件中修改父组件传递过来的数组arr,从而改变父组件的状态。

5.向子组件中传递数据时加和不加 v-bind?

对于字面量语法和动态语法,初学者可能在父组件模板中向子组件中传递数据时到底加和不加 v-bind 会感觉迷惑。

v-bind:msg = 'msg'

这是通过 v-bind 进行传递数据并且传递的数据并不是一个字面量,双引号里的解析的是一个表达式,同样也可以是实例上定义的数据和方法(其实就是引用一个变量)。

msg='浪里行舟'

这种在没有 v-bind 的模式下只能传递一个字面量,这个字面量只限于 String 类量,字符串类型。那如果想通过字面量进行数据传递时,如果想传递非String类型,必须props名前要加上v-bind,内部通过实例寻找,如果实例方没有此属性和方法,则默认为对应的数据类型。

:msg='11111' //Number 
:msg='true' //Bootlean 
:msg='()=>{console.log(1)}' //Function
:msg='{a:1}' //Object

二、事件

1.事件驱动与数据驱动

用原生JavaScript事件驱动通常是这样的流程:

  • 先通过特定的选择器查找到需要操作的节点 -> 给节点添加相应的事件监听
  • 然后用户执行某事件(点击,输入,后退等等) -> 调用 JavaScript 来修改节点

这种模式对业务来说是没有什么问题,但是从开发成本和效率来说会比较不理想,特别是在业务系统越来越庞大的时候。另一方面,找节点和修改节点这件事,效率本身就很低,因此出现了数据驱动模式。

Vue的一个核心思想是数据驱动。所谓数据驱动,是指视图是由数据驱动生成的,我们对视图的修改,不会直接操作 DOM,而是通过修改数据,其流程如下:

用户执行某个操作 -> 反馈到 VM 处理(可以导致 Model 变动) -> VM 层改变,通过绑定关系直接更新页面对应位置的数据

可以简单地理解:数据驱动不是操作节点的,而是通过虚拟的抽象数据层来直接更新页面。主要就是因为这一点,数据驱动框架才得以有较快的运行速度(因为不需要去折腾节点),并且可以应用到大型项目。

2.修饰符事件

Vue事件分为普通事件和修饰符事件,这里我们主要介绍修饰符事件。

Vue 提供了大量的修饰符封装了这些过滤和判断,让开发者少写代码,把时间都投入的业务、逻辑上,只需要通过一个修饰符去调用。我们先来思考这样问题:怎样给这个自定义组件 custom-component 绑定一个原生的 click 事件?

<custom-component>组件内容</custom-component>

如果你的回答是<custom-component @click="xxx">,那就错了。这里的 @click 是自定义事件 click,并不是原生事件 click。绑定原生的 click 是这样的:

<custom-component @click.native="xxx">组件内容</custom-component>

实际开发过程中离不开事件修饰符,常见事件修饰符有以下这些:

  • 表单修饰符

1).lazy

在默认情况下,v-model 在每次 input 事件触发后将输入框的值与数据进行同步 。你可以添加 lazy 修饰符,从而转变为使用 change事件进行同步。适用于输入完所有内容后,光标离开才更新视图的场景。

2).trim

如果要自动过滤用户输入的首尾空白字符,可以给 v-model 添加 trim 修饰符:

<input v-model.trim="msg">

这个修饰符可以过滤掉输入完密码不小心多敲了一下空格的场景。需要注意的是,它只能过滤首尾的空格!首尾,中间的是不会过滤的。

3).number

如果想自动将用户的输入值转为数值类型,可以给 v-model 添加 number 修饰符:

<input v-model.number="value" type="text" />

clipboard.png

从上面例子,可以得到如果你先输入数字,那它就会限制你输入的只能是数字。如果你先输入字符串,那它就相当于没有加.number

  • 事件修饰符
<!-- 阻止单击事件继续传播 -->
<a v-on:click.stop="doThis"></a>

<!-- 提交事件不再重载页面 -->
<form v-on:submit.prevent="onSubmit"></form>

<!-- 修饰符可以串联 -->
<a v-on:click.stop.prevent="doThat"></a>

三、插槽

插槽分为普通插槽和作用域插槽,其实两者很类似,只不过作用域插槽可以接受子组件传递过来的参数。

1.作用域插槽

我们不妨通过一个todolist的例子来了解作用域插槽。如果当item选中后,文字变为黄色(如下图所示),该如何实现呢?

clipboard.png

// 父组件
<template>
  <div class="toList">
    <input v-model="info" type="text" /> <button @click="addItem">添加</button>
    <ul>
      <TodoItem v-for="(item, index) in listData" :key="index">
        <template v-slot:item="itemProps"> // 这是个具名插槽
        // 其中itemProps的值就是子组件传递过来的对象
          <span
            :style="{
              fontSize: '20px',
              color: itemProps.checked ? 'yellow' : 'blue'
            }"
            >{{ item }}</span
          >
        </template>
      </TodoItem>
    </ul>
  </div>
</template>
<script>
import TodoItem from "./TodoItem";
export default {
  components: {
    TodoItem
  },
  data() {
    return {
      info: "",
      listData: []
    };
  },
  methods: {
    addItem() {
      this.listData.push(this.info);
      this.info = "";
    }
  }
};
</script>
// 子组件
<template>
  <div>
    <li class="item">
      <input v-model="checked" type="checkbox" />
      <slot name="item" :checked="checked"></slot> // 将checked的值传递给父组件
    </li>
  </div>
</template>
<script>
export default {
  data() {
    return {
      checked: false
    };
  }
};
</script>

值得注意:v-bind:style 的对象语法十分直观——看着非常像 CSS,但其实是一个 JavaScript 对象。CSS 属性名可以用驼峰式 (camelCase) 或短横线分隔 (kebab-case,记得用引号括起来) 来命名。

2.v-slot新语法

在 2.6.0 中,我们为具名插槽和作用域插槽引入了一个新的统一的语法 (即 v-slot 指令)。它取代了 slot 和 slot-scope 。我们来思考个问题:相同名称的插槽是合并还是替换

  • Vue2.5版本,普通插槽合并、作用域插槽替换
  • Vue2.6版本,都是替换(见下面例子)

我们通过一个例子介绍下Vue2.6版本默认插槽、具名插槽和作用域插槽的新语法:

// 父组件
<template>
  <div class="helloSlot">
    <h2>2.6 新语法</h2>
    <SlotDemo>
      <p>默认插槽:default slot</p>
      <template v-slot:title>
        <p>具名插槽:title slot1</p>
        <p>具名插槽:title slot2</p>
      </template>
      <template v-slot:title>
        <p>new具名插槽:title slot1</p>
        <p>new具名插槽:title slot2</p>
      </template>
      <template v-slot:item="props">
        <p>作用域插槽:item slot-scope {{ props }}</p>
      </template>
    </SlotDemo>
  </div>
</template>
<script>
import Slot from "./slot";
export default {
  components: {
    SlotDemo: Slot
  }
};
</script>
// 子组件
<template>
  <div>
    <slot />
    <slot name="title" />
    <slot name="item" :propData="propData" />  // propData这个属性名可以任意取
  </div>
</template>
<script>
export default {
  data() {
    return {
      propData: {
        value: "浪里行舟"
      }
    };
  }
};
</script>

slot

给大家推荐一个好用的BUG监控工具Fundebug,欢迎免费试用!

欢迎关注公众号:前端工匠,你的成长我们一起见证!
image

参考文章

查看原文

赞 153 收藏 120 评论 4

TiJay 关注了专栏 · 2019-05-23

边城客栈

全栈技术专栏

关注 3168

TiJay 赞了文章 · 2019-05-23

理解 JavaScript 的 async/await

2020-06-04 更新

JavaScript 中的 async/await 是 AsyncFunction 特性 中的关键字。目前为止,除了 IE 之外,常用浏览器和 Node (v7.6+) 都已经支持该特性。具体支持情况可以在 这里 查看。


我第一次看到 async/await 这组关键字并不是在 JavaScript 语言里,而是在 C# 5.0 的语法中。C# 的 async/await 需要在 .NET Framework 4.5 以上的版本中使用,因此我还很悲伤了一阵——为了要兼容 XP 系统,我们开发的软件不能使用高于 4.0 版本的 .NET Framework。

我之前在《闲谈异步调用“扁平”化》 中就谈到了这个问题。无论是在 C# 还是 JavaScript 中,async/await 都是非常棒的特性,它们也都是非常甜的语法糖。C# 的 async/await 实现离不开 Task 或 Task\<Result\> 类,而 JavaScript 的 async/await 实现,也离不开 Promise

现在抛开 C# 和 .NET Framework,专心研究下 JavaScript 的 async/await。

1. async 和 await 在干什么

任意一个名称都是有意义的,先从字面意思来理解。async 是“异步”的简写,而 await 可以认为是 async wait 的简写。所以应该很好理解 async 用于申明一个 function 是异步的,而 await 用于等待一个异步方法执行完成。

另外还有一个很有意思的语法规定,await 只能出现在 async 函数中。然后细心的朋友会产生一个疑问,如果 await 只能出现在 async 函数中,那这个 async 函数应该怎么调用?

如果需要通过 await 来调用一个 async 函数,那这个调用的外面必须得再包一个 async 函数,然后……进入死循环,永无出头之日……

如果 async 函数不需要 await 来调用,那 async 到底起个啥作用?

1.1. async 起什么作用

这个问题的关键在于,async 函数是怎么处理它的返回值的!

我们当然希望它能直接通过 return 语句返回我们想要的值,但是如果真是这样,似乎就没 await 什么事了。所以,写段代码来试试,看它到底会返回什么:

async function testAsync() {
    return "hello async";
}

const result = testAsync();
console.log(result);

看到输出就恍然大悟了——输出的是一个 Promise 对象。

c:\var\test> node --harmony_async_await .
Promise { 'hello async' }

所以,async 函数返回的是一个 Promise 对象。从文档中也可以得到这个信息。async 函数(包含函数语句、函数表达式、Lambda表达式)会返回一个 Promise 对象,如果在函数中 return 一个直接量,async 会把这个直接量通过 Promise.resolve() 封装成 Promise 对象。

补充知识点 [2020-06-04]

Promise.resolve(x) 可以看作是 new Promise(resolve => resolve(x)) 的简写,可以用于快速封装字面量对象或其他对象,将其封装成 Promise 实例。

async 函数返回的是一个 Promise 对象,所以在最外层不能用 await 获取其返回值的情况下,我们当然应该用原来的方式:then() 链来处理这个 Promise 对象,就像这样

testAsync().then(v => {
    console.log(v);    // 输出 hello async
});

现在回过头来想下,如果 async 函数没有返回值,又该如何?很容易想到,它会返回 Promise.resolve(undefined)

联想一下 Promise 的特点——无等待,所以在没有 await 的情况下执行 async 函数,它会立即执行,返回一个 Promise 对象,并且,绝不会阻塞后面的语句。这和普通返回 Promise 对象的函数并无二致。

那么下一个关键点就在于 await 关键字了。

1.2. await 到底在等啥

一般来说,都认为 await 是在等待一个 async 函数完成。不过按语法说明,await 等待的是一个表达式,这个表达式的计算结果是 Promise 对象或者其它值(换句话说,就是没有特殊限定)。

因为 async 函数返回一个 Promise 对象,所以 await 可以用于等待一个 async 函数的返回值——这也可以说是 await 在等 async 函数,但要清楚,它等的实际是一个返回值。注意到 await 不仅仅用于等 Promise 对象,它可以等任意表达式的结果,所以,await 后面实际是可以接普通函数调用或者直接量的。所以下面这个示例完全可以正确运行

function getSomething() {
    return "something";
}

async function testAsync() {
    return Promise.resolve("hello async");
}

async function test() {
    const v1 = await getSomething();
    const v2 = await testAsync();
    console.log(v1, v2);
}

test();

1.3. await 等到了要等的,然后呢

await 等到了它要等的东西,一个 Promise 对象,或者其它值,然后呢?我不得不先说,await 是个运算符,用于组成表达式,await 表达式的运算结果取决于它等的东西。

如果它等到的不是一个 Promise 对象,那 await 表达式的运算结果就是它等到的东西。

如果它等到的是一个 Promise 对象,await 就忙起来了,它会阻塞后面的代码,等着 Promise 对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果。

看到上面的阻塞一词,心慌了吧……放心,这就是 await 必须用在 async 函数中的原因。async 函数调用不会造成阻塞,它内部所有的阻塞都被封装在一个 Promise 对象中异步执行。

2. async/await 帮我们干了啥

2.1. 作个简单的比较

上面已经说明了 async 会将其后的函数(函数表达式或 Lambda)的返回值封装成一个 Promise 对象,而 await 会等待这个 Promise 完成,并将其 resolve 的结果返回出来。

现在举例,用 setTimeout 模拟耗时的异步操作,先来看看不用 async/await 会怎么写

function takeLongTime() {
    return new Promise(resolve => {
        setTimeout(() => resolve("long_time_value"), 1000);
    });
}

takeLongTime().then(v => {
    console.log("got", v);
});

如果改用 async/await 呢,会是这样

function takeLongTime() {
    return new Promise(resolve => {
        setTimeout(() => resolve("long_time_value"), 1000);
    });
}

async function test() {
    const v = await takeLongTime();
    console.log(v);
}

test();

眼尖的同学已经发现 takeLongTime() 没有申明为 async。实际上,takeLongTime() 本身就是返回的 Promise 对象,加不加 async 结果都一样,如果没明白,请回过头再去看看上面的“async 起什么作用”。

又一个疑问产生了,这两段代码,两种方式对异步调用的处理(实际就是对 Promise 对象的处理)差别并不明显,甚至使用 async/await 还需要多写一些代码,那它的优势到底在哪?

2.2. async/await 的优势在于处理 then 链

单一的 Promise 链并不能发现 async/await 的优势,但是,如果需要处理由多个 Promise 组成的 then 链的时候,优势就能体现出来了(很有意思,Promise 通过 then 链来解决多层回调的问题,现在又用 async/await 来进一步优化它)。

假设一个业务,分多个步骤完成,每个步骤都是异步的,而且依赖于上一个步骤的结果。我们仍然用 setTimeout 来模拟异步操作:

/**
 * 传入参数 n,表示这个函数执行的时间(毫秒)
 * 执行的结果是 n + 200,这个值将用于下一步骤
 */
function takeLongTime(n) {
    return new Promise(resolve => {
        setTimeout(() => resolve(n + 200), n);
    });
}

function step1(n) {
    console.log(`step1 with ${n}`);
    return takeLongTime(n);
}

function step2(n) {
    console.log(`step2 with ${n}`);
    return takeLongTime(n);
}

function step3(n) {
    console.log(`step3 with ${n}`);
    return takeLongTime(n);
}

现在用 Promise 方式来实现这三个步骤的处理

function doIt() {
    console.time("doIt");
    const time1 = 300;
    step1(time1)
        .then(time2 => step2(time2))
        .then(time3 => step3(time3))
        .then(result => {
            console.log(`result is ${result}`);
            console.timeEnd("doIt");
        });
}

doIt();

// c:\var\test>node --harmony_async_await .
// step1 with 300
// step2 with 500
// step3 with 700
// result is 900
// doIt: 1507.251ms

输出结果 resultstep3() 的参数 700 + 200 = 900doIt() 顺序执行了三个步骤,一共用了 300 + 500 + 700 = 1500 毫秒,和 console.time()/console.timeEnd() 计算的结果一致。

如果用 async/await 来实现呢,会是这样

async function doIt() {
    console.time("doIt");
    const time1 = 300;
    const time2 = await step1(time1);
    const time3 = await step2(time2);
    const result = await step3(time3);
    console.log(`result is ${result}`);
    console.timeEnd("doIt");
}

doIt();

结果和之前的 Promise 实现是一样的,但是这个代码看起来是不是清晰得多,几乎跟同步代码一样

2.3. 还有更酷的

现在把业务要求改一下,仍然是三个步骤,但每一个步骤都需要之前每个步骤的结果。

function step1(n) {
    console.log(`step1 with ${n}`);
    return takeLongTime(n);
}

function step2(m, n) {
    console.log(`step2 with ${m} and ${n}`);
    return takeLongTime(m + n);
}

function step3(k, m, n) {
    console.log(`step3 with ${k}, ${m} and ${n}`);
    return takeLongTime(k + m + n);
}

这回先用 async/await 来写:

async function doIt() {
    console.time("doIt");
    const time1 = 300;
    const time2 = await step1(time1);
    const time3 = await step2(time1, time2);
    const result = await step3(time1, time2, time3);
    console.log(`result is ${result}`);
    console.timeEnd("doIt");
}

doIt();

// c:\var\test>node --harmony_async_await .
// step1 with 300
// step2 with 800 = 300 + 500
// step3 with 1800 = 300 + 500 + 1000
// result is 2000
// doIt: 2907.387ms

除了觉得执行时间变长了之外,似乎和之前的示例没啥区别啊!别急,认真想想如果把它写成 Promise 方式实现会是什么样子?

function doIt() {
    console.time("doIt");
    const time1 = 300;
    step1(time1)
        .then(time2 => {
            return step2(time1, time2)
                .then(time3 => [time1, time2, time3]);
        })
        .then(times => {
            const [time1, time2, time3] = times;
            return step3(time1, time2, time3);
        })
        .then(result => {
            console.log(`result is ${result}`);
            console.timeEnd("doIt");
        });
}

doIt();

有没有感觉有点复杂的样子?那一堆参数处理,就是 Promise 方案的死穴—— 参数传递太麻烦了,看着就晕!

3. 洗洗睡吧

就目前来说,已经理解 async/await 了吧?但其实还有一些事情没提及——Promise 有可能 reject 啊,怎么处理呢?如果需要并行处理3个步骤,再等待所有结果,又该怎么处理呢?

阮一峰老师已经说过了,我就懒得说了。

4. 推荐相关文章

5. 来跟边城(作者)学 更新@2020-11-14

TypeScript从入门到实践 【2020 版】

TypeScript从入门到实践 【2020 版】

6. 关于转载 补充@2020-03-05

常有读者问是否可以转载。

笔者表示欢迎各位转载,但转载时一定注明作者和出处,谢谢!


公众号-边城客栈
请关注公众号 边城客栈

看完了先别走,点个赞啊 ⇓,赞赏 ⇘ 也行!

查看原文

赞 1301 收藏 1103 评论 134

TiJay 发布了文章 · 2019-05-18

【Node】搭建一个静态资源服务器

一个包括文件缓存、传输压缩、ejs 模版引擎、MIME 类型匹配等功能的 Node 静态资源服务器,使用 Node 的内置模块实现,可以通过链接访问资源。

一、创建 HTTP Server 服务器

Node 的 http 模块提供 HTTP 服务器和客户端接口,通过 require('http')使用。

先创建一个简单的 http server。配置参数如下:

// server/config.js
module.exports = {
  root: process.cwd(),
  host: '127.0.0.1',
  port: '8877'
}

process.cwd()方法返回 Node.js 进程的当前工作目录,和 Linus 命令pwd功能一样,

Node 服务器每次收到 HTTP 请求后都会调用 http.createServer() 这个回调函数,每次收一条请求,都会先解析请求头作为新的 request 的一部分,然后用新的 request 和 respond 对象触发回调函数。以下创建一个简单的 http 服务,先默认响应的 status 为 200:

// server/http.js
const http = require('http')
const path = require('path')

const config = require('./config')

const server = http.createServer((request, response) => {
  let filePath = path.join(config.root, request.url)
  response.statusCode = 200
  response.setHeader('content-type', 'text/html')
  response.write(`<html><body><h1>Hello World! </h1><p>${filePath}</p></body></html>`)
  response.end()
})

server.listen(config.port, config.host, () => {
  const addr = `http://${config.host}:${config.port}`
  console.info(`server started at ${addr}`)
})

客户端请求静态资源的地址可以通过request.url获得,然后使用 path 模块拼接资源的路径。

执行$ node server/http.js 后访问 http://127.0.0.1:8877/ 后的任意地址都会显示该路径:

clipboard.png

每次修改服务器响应内容,都需要重新启动服务器更新,推荐自动监视更新自动重启的插件supervisor,使用supervisor启动服务器。

$ npm install supervisor -D
$ supervisor server/http.js

二、使用 fs 读取资源文件

我们的目的是搭建一个静态资源服务器,当访问一个到资源文件或目录时,我们希望可以得到它。这时就需要使用 Node 内置的 fs 模块读取静态资源文件,

使用 fs.stat() 读取文件状态信息,通过回调中的状态stats.isFile()判断文件还是目录,并使用fs.readdir()读取目录中的文件名

// server/route.js
const fs = require('fs')

module.exports = function (request, response, filePath){
  fs.stat(filePath, (err, stats) => {
    if (err) {
      response.statusCode = 404
      response.setHeader('content-type', 'text/plain')
      response.end(`${filePath} is not a file`)
      return;
    }
    if (stats.isFile()) {
      response.statusCode = 200
      response.setHeader('content-type', 'text/plain')
      fs.createReadStream(filePath).pipe(response)
    } 
    else if (stats.isDirectory()) {
      fs.readdir(filePath, (err, files) => {
        response.statusCode = 200
        response.setHeader('content-type', 'text/plain')
        response.end(files.join(','))
      })
    }
  })
}

其中fs.createReadStream()读取文件流,pipe()是分段读取文件到内存,优化高并发的情况。

修改之前的 http server ,引入上面新建的 route.js 作为响应函数:

// server/http.js
const http = require('http')
const path = require('path')

const config = require('./config')
const route = require('./route')

const server = http.createServer((request, response) => {
  let filePath = path.join(config.root, request.url)
  route(request, response, filePath)
})

server.listen(config.port, config.host, () => {
  const addr = `http://${config.host}:${config.port}`
  console.info(`server started at ${addr}`)
})

再次执行 $ node server/http.js 如果是文件夹则显示目录:

clipboard.png

如果是文件则直接输出:

clipboard.png

成熟的静态资源服务器 anywhere,深入理解 nodejs 作者写的。

三、util.promisify 优化 fs 异步

我们注意到fs.stat()fs.readdir() 都有 callback 回调。我们结合 Node 的 util.promisify() 来链式操作,代替地狱回调。

util.promisify() 只是返回一个 Promise 实例来方便异步操作,并且可以和 async/await 配合使用,修改 route.js 中 fs 操作相关的代码:

// server/route.js
const fs = require('fs')
const util = require('util')

const stat = util.promisify(fs.stat)
const readdir = util.promisify(fs.readdir)

module.exports = async function (request, response, filePath) {
  try {
    const stats = await stat(filePath)
    if (stats.isFile()) {
      response.statusCode = 200
      response.setHeader('content-type', 'text/plain')
      fs.createReadStream(filePath).pipe(response)
    }
    else if (stats.isDirectory()) {
      const files = await readdir(filePath)
      response.statusCode = 200
      response.setHeader('content-type', 'text/plain')
      response.end(files.join(','))
    }
  } catch (err) {
    console.error(err)
    response.statusCode = 404
    response.setHeader('content-type', 'text/plain')
    response.end(`${filePath} is not a file`)
  }
}

因为 fs.stat()fs.readdir() 都可能返回 error,所以使用try-catch捕获。

使用异步时需注意,异步回调需要使用 await 返回异步操作,不加 await 返回的是一个 promise,而且 await 必须在async里面使用。

四、添加 ejs 模版引擎

从上面的例子是手工输入文件路径,然后返回资源文件。现在优化这个例子,将文件目录变成 html 的 a 链接,点击后返回文件资源。

在第一个例子中使用response.write()插入 HTML 标签,这种方式显然是不友好的。这时候就使用模版引擎做到拼接 HTML。

常用的模版引擎有很多,ejs、jade、handlebars,这里的使用ejs:

npm i ejs

新建一个模版 src/template/index.ejs ,和 html 文件很像:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Node Server</title>
</head>
<body>
<% files.forEach(function(name){ %>
  <a href="../<%= dir %>/<%= name %>"> <%= name %></a><br>
<% }) %>
</body>
</html>

再次修改 route.js,添加 ejs 模版并ejs.render(),在文件目录的代码中传递 files、dir 等参数:

// server/route.js

const fs = require('fs')
const util = require('util')
const path = require('path')
const ejs = require('ejs')
const config = require('./config')
// 异步优化
const stat = util.promisify(fs.stat)
const readdir = util.promisify(fs.readdir)
// 引入模版
const tplPath = path.join(__dirname,'../src/template/index.ejs')
const sourse = fs.readFileSync(tplPath) // 读出来的是buffer

module.exports = async function (request, response, filePath) {
  try {
    const stats = await stat(filePath)
    if (stats.isFile()) {
      response.statusCode = 200
      ···
    }
    else if (stats.isDirectory()) {
      const files = await readdir(filePath)
      response.statusCode = 200
      response.setHeader('content-type', 'text/html')
      // response.end(files.join(','))

      const dir = path.relative(config.root, filePath) // 相对于根目录
      const data = {
        files,
        dir: dir ? `${dir}` : '' // path.relative可能返回空字符串()
      }

      const template = ejs.render(sourse.toString(),data)
      response.end(template)
    }
  } catch (err) {
    response.statusCode = 404
    ···
  }
}

重启动$ node server/http.js 就可以看到文件目录的链接:

clipboard.png

五、匹配文件 MIME 类型

静态资源有图片、css、js、json、html等,
在上面判断stats.isFile()后响应头设置的 Content-Type 都为 text/plain,但各种文件有不同的 Mime 类型列表。

我们先根据文件的后缀匹配它的 MIME 类型:

// server/mime.js
const path = require('path')
const mimeTypes = {
  'js': 'application/x-javascript',
  'html': 'text/html',
  'css': 'text/css',
  'txt': "text/plain"
}

module.exports = (filePath) => {
  let ext = path.extname(filePath)
    .split('.').pop().toLowerCase() // 取扩展名

  if (!ext) { // 如果没有扩展名,例如是文件
    ext = filePath
  }
  return mimeTypes[ext] || mimeTypes['txt']
}

匹配到文件的 MIME 类型,再使用response.setHeader('Content-Type', 'XXX')设置响应头:

// server/route.js
const mime = require('./mime')
···
    if (stats.isFile()) {
      const mimeType = mime(filePath)
      response.statusCode = 200
      response.setHeader('Content-Type', mimeType)
      fs.createReadStream(filePath).pipe(response)
    }

运行 server 服务器访问一个文件,可以看到 Content-Type 修改了:

clipboard.png

六、文件传输压缩

注意到 request header 中有 Accept—Encoding:gzip,deflate,告诉服务器客户端所支持的压缩方式,响应时 response header 中使用 content-Encoding 标志文件的压缩方式。

node 内置 zlib 模块支持文件压缩。在前面文件读取使用的是fs.createReadStream(),所以压缩是对 ReadStream 文件流。示例 gzip,deflate 方式的压缩:

// server/compress.js
const  zlib = require('zlib')

module.exports = (readStream, request, response) => {
  const acceptEncoding = request.headers['accept-encoding']
  
  if (!acceptEncoding || !acceptEncoding.match(/\b(gzip|deflate)\b/)) {
    return readStream
  }
  else if (acceptEncoding.match(/\bgzip\b/)) {
    response.setHeader("Content-Encoding", 'gzip')
    return readStream.pipe(zlib.createGzip())
  }
  else if (acceptEncoding.match(/\bdeflate\b/)) {
    response.setHeader("Content-Encoding", 'deflate')
    return readStream.pipe(zlib.createDeflate())
  }
}

修改 route.js 文件读取的代码:

// server/route.js
const compress = require('./compress')
···
  if (stats.isFile()) {
      const mimeType = mime(filePath)
      response.statusCode = 200
      response.setHeader('Content-Type', mimeType)
      
      // fs.createReadStream(filePath).pipe(response)
+     let readStream = fs.createReadStream(filePath)
+     if(filePath.match(config.compress)) { // 正则匹配:/\.(html|js|css|md)/
        readStream = compress(readStream,request, response)
      }
      readStream.pipe(response)
    }

运行 server 可以看到不仅 response header 增加压缩标志,而且 3K 大小的资源压缩到了 1K,效果明显:

clipboard.png

七、资源缓存

以上的 Node 服务都是浏览器首次请求或无缓存状态下的,那如果浏览器/客户端请求过资源,一个重要的前端优化点就是缓存资源在客户端。缓存有强缓存和协商缓存

强缓存在 Request Header 中的字段是 Expires 和 Cache-Control;如果在有效期内则直接加载缓存资源,状态码直接是显示 200。

协商缓存在 Request Header 中的字段是:

  • If-Modified-Since(对应值为上次 Respond Header 中的 Last-Modified)
  • If-None—Match(对应值为上次 Respond Header 中的 Etag)

如果协商成功则返回 304 状态码,更新过期时间并加载浏览器本地资源,否则返回服务器端资源文件。

首先配置默认的 cache 字段:

// server/config.js
module.exports = {
  root: process.cwd(),
  host: '127.0.0.1',
  port: '8877',
  compress: /\.(html|js|css|md)/,
  cache: {
    maxAge: 2,
    expires: true,
    cacheControl: true,
    lastModified: true,
    etag: true
  }
}

新建 server/cache.js,设置响应头:

const config = require('./config')
function refreshRes (stats, response) {
  const {maxAge, expires, cacheControl, lastModified, etag} = config.cache;

  if (expires) {
    response.setHeader('Expires', (new Date(Date.now() + maxAge * 1000)).toUTCString());
  }
  if (cacheControl) {
    response.setHeader('Cache-Control', `public, max-age=${maxAge}`);
  }
  if (lastModified) {
    response.setHeader('Last-Modified', stats.mtime.toUTCString());
  }
  if (etag) {
    response.setHeader('ETag', `${stats.size}-${stats.mtime.toUTCString()}`); // mtime 需要转成字符串,否则在 windows 环境下会报错
  }
}

module.exports = function isFresh (stats, request, response) {
  refreshRes(stats, response);

  const lastModified = request.headers['if-modified-since'];
  const etag = request.headers['if-none-match'];

  if (!lastModified && !etag) {
    return false;
  }
  if (lastModified && lastModified !== response.getHeader('Last-Modified')) {
    return false;
  }
  if (etag && etag !== response.getHeader('ETag')) {
    return false;
  }
  return true;
};

最后修改 route.js 中的

// server/route.js
+ const isCache = require('./cache')

   if (stats.isFile()) {
      const mimeType = mime(filePath)
      response.setHeader('Content-Type', mimeType)

+     if (isCache(stats, request, response)) {
        response.statusCode = 304;
        response.end();
        return;
      }
      
      response.statusCode = 200
      // fs.createReadStream(filePath).pipe(response)
      let readStream = fs.createReadStream(filePath)
      if(filePath.match(config.compress)) {
        readStream = compress(readStream,request, response)
      }
      readStream.pipe(response)
    }

重启 node server 访问某个文件,在第一次请求成功时 Respond Header 返回缓存时间:

clipboard.png

一段时间后再次请求该资源文件,Request Header 发送协商请求字段:

clipboard.png


以上就是一个简单的 Node 静态资源服务器。可以在我的 github NodeStaticServer 上clone这个项目

图片描述

查看原文

赞 18 收藏 14 评论 1

TiJay 赞了文章 · 2019-05-14

IndexedDB 应用探索

说起 IndexedDB,大家应该会有一些疑问,比如:什么是 IndexedDB?适合什么业务场景?哪些公司哪些业务已经开始使用 indexedDB了?带着这些问题,阅读本文,相信能够给你答案。

IndexedDB 诞生背景

在开始之前,我们先简单梳理一下浏览器存储的几种方式(详见👉浏览器存储方式

会话期 Cookie持久性 CookiesessionStoragelocalStorageindexedDBWebSQL
存储大小4kb4kb2.5~10 MB2.5~10 MB>250MB已废弃
失效时间浏览器关闭自动清除设置过期时间,到期后清除浏览器关闭后清除永久保存(除非手动清除)手动更新或删除已废弃
与服务端交互已废弃
访问策略符合同源策略可以访问符合同源策略可以访问符合同源策略可以访问即使同源也不可相互访问符合同源策略可以访问已废弃

WebSQL 是一种浏览器存储方案,属于传统的关系型数据库,需要写 sql 语句查询。WebSQL 出现过一段时间,虽然已被部分浏览器支持,但又被废弃,由 IndexedDB 取代。
废弃原因:
This document was on the W3C Recommendation track but specification work has stopped. The specification reached an impasse: all interested implementors have used the same SQL backend (Sqlite), but we need multiple independent implementations to proceed along a standardisation path.
大意:
该文件是W3C推荐标准,但规范的制定工作已经停止。该规范陷入僵局:所有感兴趣的实现者都使用了相同的SQL后端(SQLite的),但我们需要多个独立的实现沿着规范化的路径进行。

cookie 和 webStorage 存储数据格式仅支持 String,存储时需要借助 JSON.stringify() 将 JSON 对象转化为字符串,读取时需要借助 JSON.parse() 将字符串转化为 JSON 对象。

一般来说,我们更推荐使用 webStroage,但其存储大小有限、数据存储仅支持 String 格式、不提供搜索功能,不能建立自定义的索引。因此,需要一种新的解决方案,这就是 IndexedDB 诞生的背景。

一、什么是 IndexedDB ?

通俗地说,IndexedDB 就是浏览器提供的本地数据库,它可以被网页脚本创建和操作。IndexedDB 允许储存大量数据,提供查找接口,还能建立索引。这些都是 LocalStorage 所不具备的。就数据库类型而言,IndexedDB 不属于关系型数据库(不支持 SQL 查询语句),更接近 NoSQL 数据库。

从 DB(Data Base) 可以看出它是一个数据库。常用的数据库有两种类型:

  • 关系型数据库:数据存储在表中,使用 sql 语句操作数据库,如:MySQLOracleWebSQL(已废弃)
  • 非关系型数据库:数据集作为 JSON 对象存储,不需要写sql 语句,如:RedisMongoDBIndexedDB

IndexedDB 是非关系型数据库,不需要写 sql 语句进行数据库操作,数据格式可使用 JSON 对象。

IndexedDB 有很多优点:

  • 存储空间大:没有存储上限,一般来说不小于 250M
  • 存储格式多样:

    • 支持字符串存储
    • 支持二进制存储(ArrayBuffer 对象和 Blob 对象)
    • 支持 JSON 键值对存储,一个对象相当于关系型数据库中的数据表,我们称其为 对象仓库 (object store)
  • 异步操作:性能更强。防止进行大量数据读写时,拖慢网页(localStorage 的操作是同步的)
  • 同源限制:每一个数据库对应一个域名。只能访问自身域名下的数据库,不能跨域访问
  • 支持事务:在一系列操作步骤之中,如果有一步失败,那么整个事务就都会取消,数据库回滚到事务发生之前的状态,不存在只改写一部分数据的情况

二、适用场景

  • cookie:短期登陆,例如:token 会过期,需要设置过期时间,过期后重新换取 token
  • sessionStorage:敏感账号一次性登录
  • localStorage:长期登录
  • indexedDB:存储大量结构化数据数据

对于简单的数据,应该继续使用 localStorage;对于大量结构化数据,indexedDB 会更适合。当然如果你需要设置过期时间的短期存储,还是使用 cookie 存储吧。

三、开始使用

基本步骤:
  • 打开(创建)数据库,并开始一个事务
  • 创建一个 object store
  • 构建一个请求来执行一些数据库操作,像增加或提取数据等
  • 通过监听正确类型的 DOM 事件以等待操作完成
  • 在操作结果上进行一些操作(可以在 request 对象中找到)

下面给出了三种使用方案,你可以用最简单的原生API 进行基本操作,也可以自己封装一个库,也可以用第三方库。

(1)基础调用

(2)自己封装

<script>
  var myDB = {
    name: 'school', // 数据库名
    version: 1, // 数据库版本号
    db: null,
    ojstore: {
      name: 'teachers', // 存储空间表的名字
      keypath: 'id', // 主键
      indexKey: 'age' // 年龄索引
    }
  }

  var INDEXDB = {
    indexedDB: window.indexedDB || window.webkitindexedDB,
    IDBKeyRange: window.IDBKeyRange || window.webkitIDBKeyRange, // 键范围
    // 打开或创建数据库,建立对象存储空间 ObjectStore
    openDB: function (dbname, dbversion) {
      var that = this
      var version = dbversion || 1
      var request = that.indexedDB.open(dbname, version)

      request.onerror = function (e) {
        console.log(e.currentTarget.error.message)
      }

      request.onsuccess = function (e) {
        myDB.db = e.target.result
        console.log('成功建立并打开数据库:' + myDB.name + 'version' + dbversion)
      }

      request.onupgradeneeded = function (e) {
        var db = e.target.result
        var transaction = e.target.transaction
        var store

        if (!db.objectStoreNames.contains(myDB.ojstore.name)) {
          //没有该对象空间时创建该对象空间
          store = db.createObjectStore(myDB.ojstore.name, {
            keyPath: myDB.ojstore.keypath
          })
          console.log('成功建立对象存储空间:' + myDB.ojstore.name)
          that.storeIndex(store, myDB.ojstore.indexKey)
        }
      }
    },
    // 删除数据库
    deletedb: function (dbname) {
      var that = this
      that.indexedDB.deleteDatabase(dbname)
      console.log(dbname + '数据库已删除')
    },
    // 关闭数据库
    closeDB: function (db) {
      db.close()
      console.log('数据库已关闭')
    },
    // 添加数据,重复添加会报错
    addData: function (db, storename, data) {
      var store = db.transaction(storename, 'readwrite').objectStore(storename)
      var request

      for(var i = 0; i < data.length; i++) {
        request = store.add(data[i])

        request.onerror = function() {
          console.error('add添加数据库中已有该数据')
        }

        request.onsuccess = function() {
          console.log('add添加数据已存入数据库')
        }
      }
    },
    // 通过游标查询记录
    cursorGetData: function (db, storename, keyRange) {
      var keyRange = keyRange || ''
      var store = db.transaction(storename, 'readwrite').objectStore(storename)
      var request = store.openCursor(keyRange)

      request.onsuccess = function (e) {
        var cursor = e.target.result

        if (cursor) { // 必须要检查
          console.log(cursor)
          cursor.continue() // 遍历了存储对象中的所有内容
        } else{
          console.log('游标查询结束')
        }
      }
    },
    // 通过索引游标查询记录
    cursorGetDataByIndex: function (db, storename, keyRange) {
      var keyRange = keyRange || ''
      var store = db.transaction(storename, 'readwrite').objectStore(storename)
      var request = store.index('age').openCursor(keyRange)

      request.onsuccess = function (e) {
        console.log('游标开始查询')
        var cursor = e.target.result

        if (cursor) {//必须要检查
          console.log(cursor)
          cursor.continue()//遍历了存储对象中的所有内容
        } else {
          console.log('游标查询结束')
        }
      }
    },
    // 通过游标更新记录
    cursorUpdateData: function (db, storename) {
      var keyRange = keyRange || ''
      var store = db.transaction(storename,'readwrite').objectStore(storename)
      var request = store.openCursor()

      request.onsuccess = function (e) {
        console.log('游标开始查询')
        var cursor = e.target.result
        var value, updateRequest

        if (cursor) { // 必须要检查
          console.log(cursor)
          if (cursor.key === 1002) {
            console.log('游标开始更新')
            value = cursor.value
            value.age = 38
            updateRequest = cursor.update(value)

            updateRequest.onerror = function () {
              console.log('游标更新失败')
            }

            updateRequest.onsuccess = function () {
              console.log('游标更新成功')
            }
          } else {
            cursor.continue()
          }
        }
      }
    },
    // 通过游标删除记录
    cursorDeleteData: function (db, storename) {
      var keyRange = keyRange || ''
      var store = db.transaction(storename, 'readwrite').objectStore(storename)
      var request = store.openCursor()

      request.onsuccess = function (e) {
        var cursor = e.target.result
        var value, deleteRequest

        if (cursor) {
          if (cursor.key === 1003) {
            deleteRequest = cursor.delete() // 请求删除当前项
            deleteRequest.onerror = function () {
              console.log('游标删除该记录失败')
            }

            deleteRequest.onsuccess = function () {
              console.log('游标删除该记录成功')
            }
          } else {
            cursor.continue()
          }
        }
      }
    },
    // 创建索引
    storeIndex: function (store, indexKey) {
      var index = store.createIndex(indexKey, indexKey, {
        unique:false
      })
      console.log('创建索引' + indexKey + '成功')
    }
  }

  var teachers = [{ 
    id:1001, 
    name:'Byron', 
    age:21 
  }, {
    id:1002, 
    name:'Frank', 
    age:22
  }, {
    id:1003, 
    name:'Aaron', 
    age:23 
  }, {
    id:1004, 
    name:'Aaron', 
    age:24 
  }, {
    id:1005, 
    name:'Byron', 
    age:24 
  }, {
    id:1006, 
    name:'Frank', 
    age:30 
  }, {
    id:1007, 
    name:'Aaron', 
    age:26 
  }, {
    id:1008, 
    name:'Aaron', 
    age:27 
  }]

  INDEXDB.openDB(myDB.name, myDB.version)

  setTimeout(function() {
    // 添加数据
    INDEXDB.addData(myDB.db, myDB.ojstore.name, teachers)

    // 游标更新数据id1002更新其age为38
    INDEXDB.cursorUpdateData(myDB.db, myDB.ojstore.name)

    // 游标删除id为1003的数据
    // INDEXDB.cursorDeleteData(myDB.db, myDB.ojstore.name)

    // 关闭数据库
    // INDEXDB.closeDB(myDB.db)

    // 删除数据库
    // INDEXDB.deletedb(myDB.db)

    /*
     *游标键范围方法调用
     */
    var IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange    

    // 查找1004对象
    // var onlyKeyRange = IDBKeyRange.only(1004)
    // INDEXDB.cursorGetData(myDB.db, myDB.ojstore.name, onlyKeyRange)
    
    // 查找从1004对象开始
    // var lowerBoundKeyRange = IDBKeyRange.lowerBound(1004)
    // INDEXDB.cursorGetData(myDB.db, myDB.ojstore.name, lowerBoundKeyRange)
    
    // 查找从1004对象开始不包括1004
    // var lowerBoundKeyRangeTrue = IDBKeyRange.lowerBound(1004, true)
    // INDEXDB.cursorGetData(myDB.db, myDB.ojstore.name, lowerBoundKeyRangeTrue)
    
    // 查找到1004对象结束
    // var upperBoundKeyRange = IDBKeyRange.upperBound(1004)
    // INDEXDB.cursorGetData(myDB.db, myDB.ojstore.name, upperBoundKeyRange)
    
    // 查找到1004对象结束不包括1004
    // var upperBoundKeyRangeTrue = IDBKeyRange.upperBound(1004, true)
    // INDEXDB.cursorGetData(myDB.db, myDB.ojstore.name, upperBoundKeyRangeTrue)
    
    // 查找到1002到1004对象
    // var boundKeyRange = IDBKeyRange.bound(1002, 1004)
    // INDEXDB.cursorGetData(myDB.db, myDB.ojstore.name, boundKeyRange)
    
    // 查找到1002到1004对象不包括1002
    // var boundKeyRangeLowerTrue = IDBKeyRange.bound(1002, 1004, true)
    // INDEXDB.cursorGetData(myDB.db, myDB.ojstore.name, boundKeyRangeLowerTrue)
    
    // 查找到1002到1004对象包括1002不包括1004
    // var boundKeyRangeUpperTrue = IDBKeyRange.bound(1002, 1004, false, true)
    // INDEXDB.cursorGetData(myDB.db, myDB.ojstore.name, boundKeyRangeUpperTrue)
    
    // 查找到1002到1004对象不包括1002不包括1004
    // var boundKeyRangeLTUT = IDBKeyRange.bound(1002, 1004, true, true)
    // INDEXDB.cursorGetData(myDB.db, myDB.ojstore.name, boundKeyRangeLTUT)

    /*
     *存储键游标查询与索引键游标查询对比
     */
    
    // 存储键游标查询
    // var onlyKeyRange = IDBKeyRange.only(1004)
    // INDEXDB.cursorGetData(myDB.db, myDB.ojstore.name, onlyKeyRange)
    
    // 索引键游标查询
    // var onlyKeyRange = IDBKeyRange.only(30)
    // INDEXDB.cursorGetDataByIndex(myDB.db, myDB.ojstore.name, onlyKeyRange)
  }, 1000)
</script>

(3)第三方库:Dexie.js

Dexie.js 是 indexedDB 封装 SDK,API 简洁强大、错误处理简单而强壮。

官方文档:https://dexie.org/
<!doctype html>
<html>
  <head>
    <!-- Include Dexie -->
    <script data-original="https://unpkg.com/dexie@latest/dist/dexie.js"></script>

    <script>
        // Define your database
        var db = new Dexie("student_database");
        db.version(1).stores({
            students: 'name'
        });

        //
        // Put some data into it
        //
        var data = {
          name: "Byron",
          shoeSize: 24
        }

        db.students.put(data).then (function (){
            //
            // Then when data is stored, read from it
            //
            return db.students.get('Nicolas');
        }).then(function (student) {
            //
            // Display the result
            //
            alert ("Nicolas has shoe size " + student.shoeSize);
        }).catch(function(error) {
           //
           // Finally don't forget to catch any error
           // that could have happened anywhere in the
           // code blocks above.
           //
           alert ("Ooops: " + error);
        });
    </script>
  </head>
</html>

四、哪些公司在使用?

IndexedDB 目前已在美团的部分业务中使用,其他公司尚不清楚,但我相信数据量较大且需要缓存的业务一定可以用到 IndexedDB。

一些想法:

IndexedDB 是否可以结合 Vuex 使用?(localStorage 或许也能满足需求)

Vuex 将我们需要共享的数据放入一个公共的变量中,以便在路由跳转时重复使用,因为路由跳转是无刷新页面的,所以数据不会丢失。但是当我们刷新或跳转页面时,数据就会丢失。这时候就可以使用 IndexedDB 或 localStorage 将数据保存在其中既能够避免数据丢失,又能避免路由跳转时不必要的存储操作。

created () {
    // 页面加载时,读取 IndexedDB
    var data = indexedDB.getData('myDB', 'myStore', 'myData')
    // var data = window.localStorage.getItem('myData')
    var storeData = Object.assign(this.$store.state, data)
    this.$store.replaceState(storeData)

    // 页面刷新时将 vuex 里的信息保存到 IndexedDB
    window.addEventListener('beforeunload', () => {
        indexedDB.updateData('myDB', this.$store.state)
        // window.localStorage.setItem(this.$store.state)
    })
}
参考文章:

http://www.ruanyifeng.com/blo...

查看原文

赞 6 收藏 2 评论 0

认证与成就

  • 获得 295 次点赞
  • 获得 4 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 4 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2018-07-11
个人主页被 1.9k 人浏览