
主攻JS
前注: 文档全文请查看 根目录的文档说明。 如果可以,请给本项目加【Star】和【Fork】持续关注。 有疑义请点击这里,发【Issues】。 实战项目示例目录 0、使用说明 安装: npm install 运行(注,这里不像之前用的 test ,而是改用了 build): npm run build 1、需求列表 基本需求: 引入jQuery(或其他类似库,之所以用 jQuery 是每个前端开发者都理应会 jQuery); 使用 less 作为 css 预处理器; 标准模块化开发; 有异步加载的模块; 使用 es6、es7 语法; 写一个登录页面作为DEMO,再写一个登录后的示例页面作为跳转后页面; 可适用于多页项目; css 文件与 图片 文件脱离(即更改 css 文件路径不影响其对图片的引用) 打包要求: 启用 hash 命名,以应对缓存问题; css 自动添加兼容性前缀; 将图片统一放到同一个文件夹下,方便管理; 将共同引入的模块单独打包出来,用于缓存,减少每次重复加载的代码量; 代码进行丑化压缩; 2、涉及到的知识 入口:设置入口文件; 出口:设置打包后的文件夹以及文件命名; babel-loader:用于将es6、es7等语法,转换为es5语法; css-loader:用于处理css文件(主要是处理图片的url); style-loader:将转换后的css文件以 style 标签形式插入 html 中; postcss-loader:一般用于添加兼容性属性前缀; less-loader:以 less 语法来写 css ; url-loader:用于将图片小于一定大小的文件,转为 base64 字符串; file-loader:url-loader 不能转换 base64字符串 的文件,被这个处理(主要用于设置打包后图片路径,以及CDN等); html-withimg-loader:用于加载html模板; html-webpack-plugin :用于将已有 html 文件作为模板,生成打包后的 html 文件; clean-webpack-plugin:用于每次打包前清理dist文件夹 CommonsChunkPlugin:提取 chunks 之间共享的通用模块 3、技术难点 3.1、多页面 多页模式是一个难点。 且不考虑共同模块(这里主要指的是html模板,而不是js的模块),光是单独每个入口 js 文件需要搭配一个相对应的 html 文件,就已经是一件很麻烦的事情了。 对于这个问题,需要借助使用 html-webpack-plugin 来实现。 由于之前木有 html-webpack-plugin 的相关内容,这里只讲思路和代码。 第一:多入口则多个html文件 也是核心内容,html-webpack-plugin 只负责生成一个 html 文件。 而多入口显然需要生成多个 html 文件,因此 有多少个入口,就需要在 webpack 的 plugins 里添加多少个 html-webpack-plugin 的实例。 同时,我们还要更改 webpack 的 entry 入口,entry 的值应该是根据入口数量自动生成的对象。 第二:chunks特性实现按需加载 通过配置 html-webpack-plugin 的 options.chunks ,可以让我们实现让 login.html 只加载 login/index.js,而 userInfo.html 只加载 userInfo/index.js(注:由于以 entry 的 key 作为寻找出口文件的根据,因此打包后带 hash 的文件名不影响匹配); 注意,这个实现的机制,是通过 options.chunk 的值,去匹配 webpack.config.js的 entry 对象的 key。 因为一个入口文件对应一个出口文件,所以这里会去拿入口文件对应的出口文件,将其加到 html 文件里。 第三:template自定义作为模板的 html 文件 options.template 可以自定义该实例以哪个 html 文件作为模板。 第四:filename options.filename 可以自定义生成的 html 文件输出为什么样的文件名。 第五:管理多入口 已知: 一个 html-webpack-plugin 实例具有以下功能: 生成一个 html 文件(一); 决定自己引入哪个 js 文件(二)(记得,webpack只负责打包js文件,不负责生成 html 文件。生成实例是依靠这个 plugins); 决定自己以哪个 html 文件作为模板(三); 决定自己打包后的目录和文件名(四); 我们通过webpack打包后,一个入口 js 文件会对应一个出口 js 文件; 而每个入口 js 文件,都对应一个 html 模板文件; 因此每个 html 模板文件,都知道自己对应哪个出口 js 文件; 所以以上是实现多入口的原理。 代码: 多入口管理文件: config/entry.json [ { "url": "login", "title": "登录" }, { "url": "userInfo", "title": "用户详细信息" } ] webpack配置文件: webpack.config.js: 首先,配置 entry: const entryJSON = require('../config/entry.json'); // 入口管理 let entry = {} entryJSON.map(page => { entry[page.url] = path.resolve(__dirname, `../src/page/${page.url}/index.js`) }) 其次,配置 plugins: // 在上面已经引用了 entryJSON const path = require('path') // 因为多入口,所以要多个HtmlWebpackPlugin,每个只能管一个入口 let plugins = entryJSON.map(page => { return new HtmlWebpackPlugin({ filename: path.resolve(__dirname, `../dist/${page.url}.html`), template: path.resolve(__dirname, `../src/page/${page.url}/index.html`), chunks: [page.url], // 实现多入口的核心,决定自己加载哪个js文件,这里的 page.url 指的是 entry 对象的 key 所对应的入口打包出来的js文件 hash: true, // 为静态资源生成hash值 minify: false, // 压缩,如果启用这个的话,需要使用html-minifier,不然会直接报错 xhtml: true, // 自闭标签 }) }) 最后,webpack 本身的配置: module.exports = { // 入口文件 entry: entry, // 出口文件 output: { path: __dirname + '/../dist', // 文件名,将打包好的导出为bundle.js filename: '[name].[hash:8].js' }, // 省略中间的配置 // 将插件添加到webpack中 plugins: plugins } 文件目录(已省略无关文件): ├─build │ └─webpack.config.js ├─dist └─src └─page ├─login │ ├─index.js │ ├─index.html │ └─login.less └─userInfo ├─index.js └─index.html 3.2、文件分类管理 如何将页面整齐的分类,也是很重要的。不合理的规划,会增加项目的维护难度。 项目目录如下分类: ├─build webpack 的配置文件,例如 webpack.config.js ├─config 跟 webpack 有关的配置文件,例如 postcss-loader 的配置文件,以及多入口管理文件 ├─dist 打包的目标文件夹,存放 html 文件 │ └─img 打包后的图片文件夹 └─src 资源文件夹 ├─common 全局配置,或者公共方法,放在此文件夹,例如 less-loader 的全局变量 ├─img 图片资源文件夹,这些是共用的图片 ├─less less 文件夹,共用的less文件 ├─page 每个页面,在page里会有一个文件夹,里面放置入口 js 文件,源 html 文件,以及不会被复用的 html template文件。 ├─template html 模板文件夹(通过js引入模板,这里的可能被复用) └─static 静态资源文件夹,这里放使用静态路径的资源 虽然还不够精细,但应对小型项目是足够了的。 3.3、别名 别名的优势很多,比如: 1、css/less 代码,可以和图片分离: 只要 webpack 配置和图片的位置不变。 那么使用别名,就可以随意移动 less 文件。 不必担心因为移动 less 文件,而造成的 less 文件与 图片 文件的相对路径改变,导致找不到图片而出错。 2、方便整体移动图片 假如原本图片放在src/img文件夹下,现在你突然想把图片放在src/image文件夹下。 如果不使用别名,你需要一个一个去修改图片的路径; 而使用别名,只需要改一下别名的路径就行了。 css-loader 支持独立于 webpack 的别名的设置,教程参照:css-loader 这里基于【3.2】的文件分类管理,附上关于别名的控制代码: { loader: 'css-loader', options: { root: path.resolve(__dirname, '../src/static'), // url里,以 / 开头的路径,去找src/static文件夹 minimize: true, // 压缩css代码 // sourceMap: true, // sourceMap,默认关闭 alias: { '@': path.resolve(__dirname, '../src/img') // '~@/logo.png' 这种写法,会去找src/img/logo.png这个文件 } } }, 其余代码已省略,如果有需要,请查看 DEMO 中的 build/webpack.config.js 文件。 3.4、安装jQuery 方案: 由于npm上并没有最新的 jQuery,目前来说, 1.7.4 是最新的版本。 所以可以从下面这个CDN直接下载 jQuery 来使用,版本是 1.12.4 https://cdn.bootcss.com/jquery/1.12.4/jquery.min.js 然后在js文件的开始位置,通过require引入(注意,不能通过 import 引入) const $ = require('../../common/jquery.min') webpack会帮你做剩下的事情,你只需要愉快的使用 jQuery 就好了。 3.5、提取 chunks 之间共享的通用模块 在 3.4 中,我们引入了 jQeury,方法简单易行,但又一个缺点,那就是会导致代码重复打包的问题。 即 jQuery 会被打包进每一个引入他的入口 js 文件中,每个页面都需要重复下载一份将jQuery代码打包到其中的 js 文件(很可能两个 js 文件只有 20kb 是自己的代码,却有 90kb 是 jQuery 代码)。 我们期望: 访问第一个页面时,预期加载 foo.js 和 jQuery.js; 访问第二个页面时,预期加载 bar.js 和 jQuery.js; 当访问第二个页面时,发现已经在第一个页面下载过 jQuery.js 了,因此将不需要再下载 jQuery 代码,只需要下载 bar.js 就可以了; 方案改进: 为了实现这个目标,毫无疑问,我们需要将 jQuery.js 文件单独打包,或者说,每一个在多个模块中共享的模块,都会被单独打包。 有几种做法,但实测后都不好用,鉴于 jQuery 会在每个页面都适用,因此综合考虑后,我采用以下方案来初步实现我的目标。 最后我采用了 webpack 自带的插件:webpack.optimize.CommonsChunkPlugin来实现,他可以将在多个文件中引入的 模块,单独打包。 关于这个插件可以先参考官方文档:[CommonsChunkPlugin: 提取 chunks 之间共享的通用模块](https://doc.webpack-china.org/plugins/commons-chunk-plugin/)。 为了实现我们的目的,我们需要做两件事: 1、使用这个插件,如下配置: const webpack = require('webpack') new webpack.optimize.CommonsChunkPlugin({ name: "foo", // 这个对应的是 entry 的 key minChunks: 2 }) 这个的效果是将至少有 2 个 chunk 引入的公共代码,打包到 foo 这个 chunk 中。 2、我们需要引入这个打包后的 chunk ,方法是通过 html-webpack-plugin 这个插件引入。 // 无关配置已经省略 new HtmlWebpackPlugin({ chunks: [page.url, 'foo'], // 这里的foo,就是通过CommonsChunkPlugin生成的chunk }) 无需修改源代码,此时我们可以执行npm run build查看打包后的效果: foo.d78e8f4193f50cc42a49.js // 199 KB(这里包含jQuery以及公共代码) login.d2819f642c5927565e7b.js // 15 KB userInfo.1610748fb3346bcd0c47.js // 4 KB 0.fe5c2c427675e10b0d3a.js // 2 KB 注: 如果页面很多的话,那么很可能某些公共组建被大量chunk所共享,而某些chunk又被少量chunk所共享。 因此可能需要特殊配置 minChunks 这个属性,具体请查看官方文档。 3.6、每次打包前,清理dist文件夹 需要借助 clean-webpack-plugin 这个插件。 使用这个插件后,可以在每次打包前清理掉整个文件夹。 基于本项目来说,清除的时候配置的时候需要这样配置: new CleanWebpackPlugin(path.resolve(__dirname, '../dist'), { root: path.resolve(__dirname, '../'), // 设置root verbose: true }) 原因在于,这个插件会认为webpack.config.js所在的目录为项目的根目录。 只使用第一个参数的话,会报错移除目标的目录位置不对: clean-webpack-plugin: (略)【实战5】打包一个具有常见功能的多页项目\dist is outside of the project root. Skipping... 而添加了第二个参数的设置后,就可以正常使用了。 注: 他的效果是直接删除文件夹,因此千万别写错目录了,如果删除了你正常的文件夹,那么……就只能哭啦。 3.7、使用 html 模板 由于我们很可能在 html 中使用 <img> 标签, 而 html-webpack-plugin 这个插件,只能用于将某个 html 文件作为打包后的源 html 文件, 不会将其 <img> 标签中的 src属性转为打包后的图片路径,同时也不会将引入的图片进行打包。 因此我们需要将 html 内容单独拆出来,page 文件夹里的源文件只负责作为 html 模板而已。 为了使用 html 模板,我们需要专门引入一个插件: html-withimg-loader:用于解析 html 文件。 使用方法很简单: 配置loader(参照 webpack.config.js); import 导入 html 模板文件(例如 login.html); 导入的时候,是一个字符串,并且图片的 url 已经被解析了。然后我们将其引入源 html 文件中(比如page/login.html),再写各种逻辑就行了。 注: 务必记得先把 html 模板插入页面中,再写他的相关逻辑。 3.8、代码的丑化压缩 使用插件 UglifyjsWebpackPlugin ,文档参照 (UglifyjsWebpackPlugin)[https://doc.webpack-china.org/plugins/uglifyjs-webpack-plugin] 压缩前: 0.fe5c2c427675e10b0d3a.js // 2 KB foo.a5e497953a435f418876.js // 199 KB login.9698d39e5b8f6c381649.js // 15 KB userInfo.f5a705ffcb43780bb3d6.js // 4 KB 丑化压缩后: 0.fe5c2c427675e10b0d3a.js // 1 KB foo.a5e497953a435f418876.js // 120 KB login.9698d39e5b8f6c381649.js // 10 KB userInfo.f5a705ffcb43780bb3d6.js // 2 KB 4、分析 重新列出所有需求: 基本需求: 引入jQuery(或其他类似库,之所以用 jQuery 是每个前端开发者都理应会 jQuery); 使用 less 作为 css 预处理器; 标准模块化开发; 有异步加载的模块; 使用 es6、es7 语法; 写一个登录页面作为DEMO,再写一个登录后的示例页面作为跳转后页面; 可适用于多页项目; css 文件与 图片 文件脱离(即更改 css 文件路径不影响其对图片的引用) 打包要求: 启用 hash 命名,以应对缓存问题; css 自动添加兼容性前缀; 将图片统一放到同一个文件夹下,方便管理; 将共同引入的模块单独打包出来,用于缓存,减少每次重复加载的代码量; 代码进行丑化压缩; 需求的实现: 基本需求: 需求的实现过程 需求 实现方法 引入jQuery 1. 通过 require() 引入,并通过 CommonsChunkPlugin 实现单独打包; 使用 less 作为 css 预处理器 1. 使用 less-loader 来处理 .less 文件; 标准模块化开发 1. 使用 import 和 require 语法来进行模块化开发; 有异步加载的模块 1. 通过 require([], callback) 来实现模块的异步加载 使用 es6、es7 语法 1. 使用 babel 来转义 写一个登录页面作为DEMO,再写一个登录后的示例页面作为跳转后页面 1. 登录页:page/login2. 跳转后页面:page/userInfo 可适用于多页项目 1. config/entry.json 用于配置多页入口;2. html-withimg-loader 来生成多页模板;3. 最后在webpack.config.js里配置 entry 和 plugins css 文件与 图片 文件脱离(即更改 css 文件路径不影响其对图片的引用) 通过 css-loader 的别名实现 打包需求: 需求的实现过程 需求 实现方法 启用 hash 命名,以应对缓存问题 配置 output 的 filename 属性,加 [chunkhash] 即可 css 自动添加兼容性前缀 使用 post-loader 的 autoprefixer 将图片统一放到同一个文件夹下,方便管理 配置 url-loader (实质是 file-loader )的 outputPath 将共同引入的模块单独打包出来,用于缓存,减少每次重复加载的代码量 使用插件 CommonsChunkPlugin 来实现 代码进行丑化压缩 使用插件 UglifyjsWebpackPlugin 来实现
POSTCSS-LOADER配置简述 前注: 文档全文请查看 根目录的文档说明。 如果可以,请给本项目加【Star】和【Fork】持续关注。 有疑义请点击这里,发【Issues】。 DEMO地址 1、概述 postcss-loader 用于处理css代码,具有下列特点: 通常由 options 和 plugins 两部分组成,plugins 虽然嵌套在 options 里,但实际上是通过其他插件生效的; 配置是可以独立的(每个配置的插件也是独立的)。详细介绍阅读【2.1】; 还有一些自定义配置,但由于篇幅所限,这里就不像之前那样详解每个配置了(主要是很多都依赖于其他东西)。 只写一些常用功能。 2、配置 2.1、独立配置 所谓独立配置,指的是在js文件中,引入的css文件如何被postcss-loader解析,取决于和他最近的那一个postcss的设置文件。 注: 对在css文件中,通过@import导入的css文件无效: 必须是通过通过import引入到js里面的css文件,才会被postcss-loader解析生效; 如果是a.css,通过@import './b.css'引入b.css文件,那么该配置对a.css生效,对b.css无效; 我查了很多资料,目前没找到能让postcss-loader对在css文件中,通过@import方式导入其他的css文件,进行生效的方法。如果有,请提 issues 给我。 优先级: 在 webpack.config.js 中的module.rules属性里设置的优先级最高; 然后按顺序找,离css文件最近的postcss.config.js配置文件,遇见的第一个文件其次; 按顺序找的后面的文件优先级最低; 找不到配置会报错; 注(完) 先假设 webpack.config.js 里配置方式如下(无任何特殊配置): // ...略略略 { test: /\.css$/, use: [ 'style-loader', 'css-loader', 'postcss-loader' ] } // ...略略略 简单来说,postcss-loader 的配置文件名为:postcss.config.js。 假设文件树结构如下: . |____app.js |____webpack.config.js |____index.html |____postcss.config.js // 1#设置文件 |____style | |____postcss.config.js // 2#设置文件 | |____style.css |____style2 | |____bar.css | |____postcss.config.js // 3#设置文件 引用(import)结构是: app.js -> style/style.css app.js -> style2/bar.css 假如两个css文件都有一条css属性:box-sizing: border-box;; 然后 style/postcss.config.js (2#)的设置如下(兼容性配置): module.exports = { plugins: [ require('autoprefixer')({ browsers: [ // 加这个后可以出现额外的兼容性前缀 "> 0.01%" ] }) ] } style2/postcss.config.js (3#)的设置如下(默认配置): module.exports = {} 经过postcss-loader的处理之后,有兼容性配置的css文件,其插入html文件后,css属性变为如下: -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; 无兼容性配置的css文件,其插入html文件后,css属性变为如下: box-sizing: border-box; 说明一点,对于postcss-loader来说,他优先取同目录下的postcss.config.js的配置属性。 另外,由于2#和3#设置文件的存在,因此无论1#如何设置,都不会影响其效果。 假如css文件找不到同目录下的postcss.config.js文件,那么会依次往上级目录寻找,直到找到,或者抵达项目根目录为止(以上面这个目录结构为例,即webpack.config.js所在目录是根目录) 2.2、自定义配置文件路径 名称 类型 默认值 描述 config {Object} undefined Set postcss.config.js config path && ctx 在上面,我们写了postcss-loader的配置文件的使用方式,分别是:【写在webpack.config.js中】,【配置文件放在对应的css文件的同级目录或者上级目录】。 但是假如我们需要统一管理 postcss-loader 的配置文件,那么就需要通过 config 来处理。 示例代码如下: { loader: 'postcss-loader', options: { config: { path: './config' // 写到目录即可,文件名强制要求是postcss.config.js } } } 表示会去 webpack.config.js 的同目录下去找文件夹 config,然后在该文件夹下找到 postcss.config.js 文件(文件名不能改变),从而读取配置。 假如这么写,会导致【放在对应的css文件,的同级目录或者上级目录,的postcss-loader的配置文件失效】。原因是优先级问题。 除此之外,还有一个context设置,略略略。 2.3、sourceMap 测试后,无效(开启与否文件大小不变) 3、插件 除了 autoprefixer 用于加兼容性前缀,其他基本都有更好的,比如stylelint不如用eslint系列替代。 3.1、autoprefixer 这个是最应该添加的插件了。 效果是对css文件添加兼容性前缀。 安装: npm install autoprefixer --save 官方github地址: https://github.com/postcss/autoprefixer 使用方式: // postcss.config.js let autoprefixer = require('autoprefixer'); module.exports = { plugins: [ autoprefixer({ browsers: [ // 加这个后可以出现额外的兼容性前缀 "> 0.01%" ] }) ] } 效果: 应该是兼容性最强的配置方法了,例如box-sizing可以添加两个前缀,有些特性可以添加三个前缀,如下: 转换前 transform: rotate(0deg); 转换后: -webkit-transform: rotate(0deg); -moz-transform: rotate(0deg); -o-transform: rotate(0deg); transform: rotate(0deg); 名称 类型 默认值 描述 其他特性: 不仅可以添加前缀,也可以删除旧前缀(过时前缀)等。详细查看官方文档。 非特殊要求,直接使用上面这个配置就行了(如果不需要最多的前缀,可以把上面的改为 autoprefixer({ browsers: [ // 加这个后可以出现额外的兼容性前缀 "> 1%" ] }) 3、参考文章 PostCSS配置指北
LESS-LOADER配置简述 前注: 文档全文请查看 根目录的文档说明。 如果可以,请给本项目加【Star】和【Fork】持续关注。 有疑义请点击这里,发【Issues】。 DEMO地址 1、概述 less-loader 用于处理编译 .less 文件,将其转为 css文件代码。 使用 less-loader 的话,必须安装 less,单独一个 less-loader 是没办法正常使用的。 安装 npm install --save less-loader less 2、配置 2.1、无任何配置 less-loader 不使用任何配置的时候,也可以正常使用。但需要配合 style-loader 和 css-loader 一起。 示例配置(其他略,参照github上的示例DEMO): { test: /\.less$/, use: [ 'style-loader', 'css-loader', 'less-loader' ] } less文件: @hundred: 100px; #app { position: relative; width: 500px; height: 500px; border: 1px solid red; background: url('./logo.png') no-repeat; box-sizing: border-box; .top { position: absolute; top: 0; left: @hundred; right: @hundred; height: @hundred; border: 2px dotted green; } .bottom { position: absolute; bottom: 0; left: @hundred; right: @hundred; height: @hundred; border: 2px dotted green; } } 编译后结果: #app { position: relative; width: 500px; height: 500px; border: 1px solid red; background: url(fb05d05e8b958e9341f72003afbffed3.png) no-repeat; box-sizing: border-box; } #app .top { position: absolute; top: 0; left: 100px; right: 100px; height: 100px; border: 2px dotted green; } #app .bottom { position: absolute; bottom: 0; left: 100px; right: 100px; height: 100px; border: 2px dotted green; } 说明运行正常。 【注一】 .less 可以通过 @import 来引入其他的 .less 文件 或 .css 文件; 引入的less文件会和之前的less文件同一个<style>标签,而引入的css文件会变成新标签; 【注二】 如果想要实现如下引用顺序: .css 引用 .less; 那么必须在解析 .css 文件的时候,配置less-loader,配置如下: { test: /\.css$/, use: [ 'style-loader', 'css-loader', 'less-loader' // compiles Less to CSS ] } 当以css文件作为入口时,起作用的是上面这个配置,而不是 test: /\.less$/, 这个配置了。 2.2、globalVars 名称 类型 默认值 描述 globalVars {Object} undefined 声明全局变量 正常使用less的全局变量没什么好说的(创建一个用于配置全局样式的 .less 文件,然后在需要使用全局样式的 .less文件里引用他)。 当然,这种方式很麻烦。 但是,less-loader里还提供了另外一种使用全局变量的方式,即在options.globalVars里进行配置。 示例代码: //webpack.config.js ... test: /\.less$/, use: [ 'style-loader', 'css-loader', { loader: 'less-loader', // compiles Less to CSS options: { // 这里配置全局变量 globalVars: { 'ten': '10px', // ten可以是ten,也可以是@ten,效果一样,下同 'hundred': '100px' } } } ] ... // style.less ... height: @hundred; ... border: @ten dotted green; ... 编译后变为: ... height: 100px; ... border: 10px dotted green; ... 【注】 globalVars 的 key ,前面有没有 @ 的效果是一样的; 用 modifyVars 替代 globalVars 的效果似乎是一样的(简单测试后如此,但不确定); 2.2、paths 名称 类型 默认值 描述 paths {Array} undefined less文件里,使用独立的文件解析路径 解释: 首先,这个不影响 js 文件导入 less 文件,只是影响 less 文件引入其他 less 文件(不包括图片文件等); 值是数组,数组的元素的类型是字符串(但实测中,数组只有第一个元素生效,其他似乎会被忽视); 数组的元素的值,需要是文件夹的【绝对路径】; 假如图片/文件在使用非相对路径时,例如:是 bar.less 时,less-loader 会去 paths 数组里第一个元素描述的绝对路径,去找对应的文件; 使用 paths 时,路径寻找顺序: 1、假如配置如下: paths: [ path.resolve(__dirname, "test") ] 2、less文件里,这么引用其他 .less 文件: @import 'foo.less'; 3、webpack的寻找顺序是: 在同目录下寻找 'foo.less',没有找到的话进入下一步; 在 path.resolve(__dirname, "test") ,即 webpack.config.js 的同目录中的 test 文件夹里,去找 foo.less,没有找到的话进入下一步; 最后,会在执行shell命令的文件夹中,去找 foo.less这个文件; 如果以上都找不到,那么会报错。
FILE-LOADER配置简述 前注: 文档全文请查看 根目录的文档说明。 如果可以,请给本项目加【Star】和【Fork】持续关注。 有疑义请点击这里,发【Issues】。 DEMO地址 1、概述 简单来说,file-loader 就是将文件(由于一般是图片文件为主,所以下面通常使用图片两字作为替代,方便理解。其他的包括字体文件等),在进行一些处理后(主要是处理文件名和路径),移动打包后的目录中。 处理的内容包括: 文件名的处理,比如加 [hash] ; 路径的处理,比如【把图片文件统一放到img文件夹中】; 优点: 相较于 url-loader 可以将图片转为base64字符串,file-loader 在功能上更加强大一些; 缺点: 实际开发中,将一定大小以下的图片转为 base64字符串,有利于加载速度的提升。 2、配置 2.1、name 名称 类型 默认值 描述 name {String|Function} [hash].[ext] 为你的文件配置自定义文件名模板 简单的来说,这个就是规定,如何命名打包后的文件夹的文件名的。 默认值表示:命名是 哈希值 + 扩展名 的形式。 常见命名方式是:img/[hash].[ext],即将所有的图片(准确的说,是被file-loader处理的文件),都打包到 img 文件夹下。 几点: [hash:6]可以控制 hash 值的长度,6 表示长度为6,默认是 32; [ext] 表示是原文件的扩展名,应该没人会想改这个吧? [path] 不好用一句话概括。举个例子,图片在 /src/logo.png,打包后文件夹是 dist,配置为 '[path][name].[ext]',那么图片最终为:/dist/src/logo.png。实际上是相对于context的路径,context默认是webpack.config.js 的路径; [name] 表示原文件的文件名(不含后缀名)。例如 logo.png 就是指 logo,但一般不推荐用这个,或者就算用这个,也要加上 [hash],不然不同文件夹有同名文件就出问题了; [hash] 的全部实际为:[<hashType>:hash:<digestType>:<length>],中间用冒号连接,除了 hash 都可以省略,通常使用默认的就行了,顶多带个长度来限制文件名长度。 2.2、context 名称 类型 默认值 描述 context {String} this.options.context 配置自定义文件 context,默认为 webpack.config.js context 简单暴力的说,影响 name 中的 [path], 举例: 根目录文件夹名为:file_loader; 图片路径:src/logo.png; 打包文件夹是:dist; 配置为:context: __dirname + '/../',name: '[path][name].[ext]'; 打包结果:dist/file_loader/src/logo.png; 2.3、publicPath 名称 类型 默认值 描述 publicPath {String|Function} __webpack_public_path__ 为你的文件配置自定义 public 发布目录 publicPath 这个一般会用webpack本身配置的,和那个效果也一样,但假如你想单独配置,就用这个。 举例: 假如,你计划把图片打包到放到CDN,我随便举个例子:https://www.abc.com/img这个目录下; 由于 CDN 和你本地服务器的网址肯定不同,所以你显然是需要通过绝对路径来加载这个图片的; 假如,图片名字为:logo.png(为了方便理解,我不加[hash]),那么预期图片的 url 为:https://www.abc.com/img/logo.png; 那么,你这样配置就可以了:publicPath: 'https://www.abc.cn/img/',name: '[name].[ext]' 于是,图片被打包到img文件夹下,加载该图片的链接是:https://www.abc.cn/img/logo.png; 最后,你把img文件夹整个丢到 CDN 上,就ok啦; 2.4、outputPath 名称 类型 默认值 描述 outputPath {String|Function} 'undefined' 为你的文件配置自定义 output 输出目录 这个就更简单了,就是相当于在name之前加了一个文件夹路径; 示例代码: name: '[name].[ext]', // 文件名,这个是将图片放在打包后的img文件夹中 publicPath: 'https://www.abc.cn/img/', outputPath: 'myImage/' // 这里记得后面要加一个斜杠 图片路径为:src/logo.png,打包后引用该图片的 url 变为:https://www.abc.cn/img/myImage/logo.png 效果和以下配置是一样的: name: 'myImage/[name].[ext]', // 文件名,这个是将图片放在打包后的img文件夹中 publicPath: 'https://www.abc.cn/img/', 但优点在于,这个属性可以配为函数,因为是函数,所以就可以判断环境,然后返回不同的值; 当然,name 也可以实现(写成一个函数的返回值,例如 name: getName()),但毕竟不好看,对吧; 注: 1、如果要写成函数,应该写成如下形式: outputPath: function (fileName) { return 'myImage/' + fileName // 后面要拼上这个 fileName 才行 } 2.5、useRelativePath 名称 类型 默认值 描述 useRelativePath {Boolean} false 如果你希望为每个文件生成一个相对 url 的 context 时,应该将其设置为 true 一般不启用这个。 至于效果,简单来说,当这个开关打开时: 首先会获取源代码中,图片文件,相对于css文件的路径关系; 然后打包后,css 代码通常会被打包到 js 文件中,于是根据之前所获取的【路径关系】,来保存打包好的图片文件; 举例来说: 图片路径:src/img/logo.png; css 路径:src/style/style.css; useRelativePath 设为 true; css被打包到js后,js的文件路径:dist/dist.js; 打包后的图片路径:img/logo.png; 原因是图片相对于css的路径关系是:css文件的上级目录的img文件夹中命名为logo.png; 2.6、emitFile 名称 类型 默认值 描述 emitFile {Boolean} true 默认情况下会生成文件,可以通过将此项设置为 false 来禁止(例如,使用了服务端的 packages) 简单粗暴的说,这个设置为 false 后,除了图片不会被打包出来,其他都按正常的来。
URL-LOADER配置简述 前注: 文档全文请查看 根目录的文档说明。 如果可以,请给本项目加【Star】和【Fork】持续关注。 有疑义请点击这里,发【Issues】。 DEMO地址 1、概述 简单来说,url-loader的效果类似file-loader。 优点: 可以将css文件中的图片链接,转为base64字符串,或移动到打包后文件夹; 缺点: 可配置性比file-loader弱一些,但其实file-loader的那些配置,一般也用不到。 2、配置 2.1、limit 名称 类型 默认值 描述 limit {Number} undefined Byte limit to inline files as Data URL 使用url-loader的唯一目的,可以说就是为了这个,效果是将文件大小低于指定值的图片,转为base64字符串。 值表示小于这个大小的图片会被转码,单位是字节(1024 即 1KB) 配置: { test: /\.(png|jpg|jpeg|gif)$/, use: [ { loader: 'url-loader', options: { limit: 10000 } } ] } css文件: #app { position: relative; width: 500px; height: 500px; border: 1px solid red; background: url('./logo.png') no-repeat; box-sizing: border-box; } #logo { position: relative; width: 100px; height: 100px; border: 1px solid red; background: url('./logo.jpg') no-repeat; box-sizing: border-box; } webpack打包后效果: url('./logo.jpg') 和 url('./logo.png') 变为 url(很长一个base64字符串) 注: 如果你想 .png 文件小于8kb转为base64字符串,但是 .jpg文件不管大小多少,都不转为base64字符串; 那么就需要用 file-loader 来搬运 .jpg 文件, url-loader 来搬运和转码 .png文件; 不能尝试两次调用 url-loader 来,用两个不同的配置来同时处理两种情况; 不过这个场景应该出现的极少。 2.2、mimetype 名称 类型 默认值 描述 mimetype {String} extname Specify MIME type for the file (Otherwise it's inferred from the file extension) 这个配置的意思呢,就是说,要不要把其他后缀名的图片文件,统一转为同一种格式的base64编码。 例如: 假如我有一个logo.png和一个logo.jpg图片; 那么png文件转码后的开头部分是:data:image/png;base64,; 而jpg文件转码后的开头部分是:data:image/jpeg;base64,; 如果配置这么写:mimetype: 'image/png'; 那么开头部分将统一变为:data:image/png;base64,; 另外,这个改变只是修改开头部分,但是实际大小是不影响的(当然,jpeg要比png多一个字符,实际测试结果,表示差别只有这一个字符而已);
STYLE-LOADER详细使用说明 前注: 文档全文请查看 根目录的文档说明。 如果可以,请给本项目加【Star】和【Fork】持续关注。 有疑义请点击这里,发【Issues】。 DEMO地址 1、概述 简单来说,style-loader是将css-loader打包好的css代码以<style>标签的形式插入到html文件中。 对于简单项目,打包然后插入也就足够了,但是遇见复杂情况,例如: 需要使用webpack的服务器热加载服务进行特殊配置; 对css文件二次处理(更改类名,添加额外css属性之类); 合并 <style> 标签(默认是不合并的); 启用sourceMap等(虽然实际根本无效嘛); 路径转换(相对路径转为绝对路径); 给 <style> 标签添加自定义属性; 手动挂载、移除 <style> 标签等; 显然就不行了。 所以需要通过配置来进行设置。 2、配置 有几个属性需要和其他东西(比如某些loader)配合,才能生效。 所以先介绍功能明确的几个,再简述很难直接应用的几个。 2.0、普通 导入方式有两种 直接import 'foo.css'; es6语法 import foo from 'foo.css'; 前者没啥好说的。 当使用后者导入时,有一些特殊特性: 在使用局部作用域时(css-loader的modules属性的应用),会有生成的(局部)标识符(identifier)。,可以通过foo.className来获取 当使用useable特性时,可以通过foo.use()以及foo.unuse()来让css生效/失效; url 特性尝试失败。 2.1、attrs 名称 类型 默认值 描述 attrs {Object} {} 添加自定义 attrs 到 style 标签 attrs 属性最好理解。 attrs的值是一个对象; 对象 key , val 成对出现的; 插入形式是以 key=val的形式插入; 但例如 [name] 或者 [hash] 之类的,无效; 例如: { loader: 'style-loader', options: { attrs: { id: 'foo' } } } 插入到html后,style标签变为如下形式:<style id="foo" type="text/css">css代码略</style>。 但是缺点是不能变为哈希值,所以如果想要实现css的局部作用域,还需要其他东西配合(这里略略略)。 2.2、transform 名称 类型 默认值 描述 transform {Function} false 转换/条件加载 CSS,通过传递转换/条件函数 简单来说,这个是拿到以字符串的形式拿到css文件,然后将这个字符串以参数的形式传给处理函数,函数处理完后返回,返回值即实际插入style标签的内容。 使用方法: 1、配置 style-loader 的属性如下: { loader: 'style-loader', options: { transform: 'transform.js' // 可以使用相对路径,这里表示跟 webpack.config.js 同目录 } } 2、在webpack.config.js的同一个目录下创建文件:transform.js(即上面写的那个路径) 3、在transform.js文件内,粘贴如下代码(CommonJS模块形式): // 这里只有一个参数,即css字符串 module.exports = function (css) { console.log(css) const transformed = css.replace(/}/g, 'box-sizing: border-box;\n}') return transformed } 这段代码的作用,相当于给每个css样式里,添加了一个box-sizing: border-box;属性。 例如css文件如下: // foo.css 转换前 #app { position: relative; } 转换完后的结果变为: // foo.css 转换后 #app { position: relative; box-sizing: border-box; } box-sizing:前面没有空格,是因为转换函数里,replace第二个参数的box-sizing:前没有空格。 4、每个css文件都会执行一次这段代码。css字符串,不包含@import导入的css文件相关的几行代码; 5、重要:这段代码执行的时间不在打包的时候,而是在插入到html文件中的时候。 这意味着你可以取得一些根据当前浏览器环境设置的值。例如通过document.body.clientWidth拿到浏览器宽度,然后动态计算一些css属性是否插入到页面中(响应式). 应用: 当以字符串形式拿到css代码的时候,我们可以做很多事情。我举几个例子: 1、判断当前浏览器环境,当需要额外兼容代码的时候,给css属性添加兼容性代码。 例如遇见box-sizing,添加-webkit-box-sizing:和-moz-box-sizing:。 2、可以进行风格设置。 例如同时存在亮色和暗色风格,用户使用的风格在设置后存在cookies或者localStorage,那么写两套代码显然是比较麻烦的。 就可以引入一个颜色映射表(暗色的值->亮色的值),默认使用暗色的值。 当检查到用户使用亮色风格时(读取cookies或者localStorage),通过颜色映射表,利用 replace 函数,将颜色值替换为亮色的。 2.3、insertAt和insertInto 名称 类型 默认值 描述 insertAt {String|Object} bottom 在给定位置处插入style标签 insertInto {String} 给定位置中插入style标签 简单来说,insertAt 和 insertInto 共通决定style标签插入哪里。 两种情况: insertAt 值为 string 类型。可以是 top 或者 bottom,表示插入某个标签 内 的顶部或者结尾,和该标签是父子关系; insertAt 值为 object 类型。key只能是 before(见 node_modules/style-loader/lib/addStyles.js 第173行),表示插入到某个标签之前(和该标签是兄弟关系),例如以下: insertAt: { before: '#app' }, insertInto: 'body' 以上代码表示,先在<body> 标签能找到<div id='app'></div>这个标签,然后插入到这个标签之前; 假如找不到符合要求的标签,则默认插入到 <head></head> 标签的末尾。 整个插入逻辑如下: 假如 insertAt 是值是 top 或者 bottom ,那么 style 标签将插入到 insertInto 所指向的DOM(通过document.querySelector(target)获取)的开头或末尾( style 标签为指向DOM的子元素); 假如 insertAt 的值是对象,那么则插入 insertInto 的子元素的 insertAt.before 所指向的DOM之前(即 document.querySelector("insertInto insertAt.before") 指向的DOM)。 注意,两个属性的标签选择器,都是通过 document.querySelector 实现的,所以存在两个问题: 属性的值,需要符合 document.querySelector 的语法; 低版本浏览器(比如IE)可能不支持这个选择器API; 2.4、sourceMap和convertToAbsoluteUrls 名称 类型 默认值 描述 sourceMap {Boolean} false 启用/禁用 Sourcemap convertToAbsoluteUrls {Boolean} false 启用 source map 后,将相对 URL 转换为绝对 URL 首先,sourceMap 实测和翻源代码后,感觉没有生效。 在跟了一遍代码后,推测原因在于,sourceMap的取值,取的是 css-laoder 的sourceMap的值。 准确的说,在webpack里,css文件被视为一个模块,因此import引入的css文件,也是一个模块对象。而在判断的时候,取的是这个模块(是一个object)的属性sourceMap的值,而不是 options.sourceMap 的值。 我已经提了issues给官方了。 而这个模块的值,推测是被css-loader的sourceMap属性赋值的(我没有去跟源代码,但测试后推断就是这样的)。 其次,从相对路径转为绝对路径,是在前端通过js代码转换的。 第三,convertToAbsoluteUrls 的效果如官方描述一样,具体下面举例。 几种情况如下(截止style-loader版本0.19.0): 当convertToAbsoluteUrls 值为false时,依然使用相对路径,即例如./foo.png; 当 css-loader 的 sourceMap 的值为true,且convertToAbsoluteUrls 值为true时,更改为绝对路径,即例如http://127.0.0.1:8080/foo.png; 可能是bug,所以目前不受style-loader的sourceMap属性的影响; bug的demo如链接 2.5、useable 作用 让所有引入的css文件,变为手动加载; 使用方法 loader: 'style-loader/useable' 说明: 当使用这个特性时,transform属性失效(打包正常,但挂载的时候表示会报错) 示例代码如下,效果解释: 用一个变量标记当前是否挂载,点击后进行判断; 如果挂载了,通过style.unref()从DOM树中移出(这些样式会失效); 如果没有挂载,那么通过style.ref()插入到DOM树中进行挂载(样式生效) 代码: // app.js import style from './style/style.css' /* useable(开始) */ let isUse = false // 这是一个按钮的点击事件 document.querySelector('#test').onclick = function () { if (isUse) { style.unref() } else { style.ref() } isUse = !isUse } /* useable(结束) */ 2.6、singleton 作用 使用一个的 <style> 标签加载所有css属性,或者是每个css模块一个 <style> 标签。 默认值是false(每个css模块一个 <style> 标签); 使用方法 { loader: 'style-loader', options: { singleton: true } } 说明: 默认情况下,我们会发现,每一个css文件会变为一个<style>标签,因此实际加载中,可能有多个<style>标签; 默认值为 false (而不是原文中说的默认情况下启用此选项) 当该值设置为 true 时,那么,原本多个<style>标签会被合并成一个<style>标签。 注: 官方文档上写的是默认开启,实际上是不开启的(v0.19.0)。我已经提了issues 2.7、hmr 名称 类型 默认值 描述 hmr {Boolean} true Enable/disable Hot Module Replacement (HMR), if disabled no HMR Code will be added (good for non local development/production) 谷歌翻译为中文: 启用/禁用热模块更换(HMR),如果禁用,则不会添加HMR代码。 这可以用于非本地开发和生产。 大概就是指默认为true时,允许热加载(就是你改了代码后,不需要刷新页面,立刻更新数据),我没实际测试过,不过应该没人会把这个设置为false吧? 2.8、base 名称 类型 默认值 描述 base {Number} true 设置模块 ID 基础 (DLLPlugin) 当使用一个或多个 DllPlugin 时,此设置主要用作 css 冲突 的修补方案。base 可以防止 app 的 css(或 DllPlugin2 的 css)覆盖 DllPlugin1 的 css,方法是指定一个 css 模块的 id 大于 DllPlugin1 的范围,例如: 等我搞清楚 DllPlugin 是什么再说吧,略略略。
CSS-LOADER配置详解 前注: 文档全文请查看 根目录的文档说明。 如果可以,请给本项目加【Star】和【Fork】持续关注。 有疑义请点击这里,发【Issues】。 1、概述 对于一般的css文件,我们需要动用三个loader(是不是觉得好麻烦); 1、css-loader: 先附上官网文档(中文)的链接:css-loader文档。 不过说实话,这个官方文档讲的很糟糕,看的人一脸懵逼。 css-loader主要用于处理图片路径(其实也包括例如导入css文件的路径),并且会将css样式打包进js文件中(以模块的形式打包导入); 但问题在于,他不会将这些代码插入html中,因此还需要通过例如style-loader来实现将打包好的css代码插入html文件中。 2、style-loader: 同样先附上官网文档(中文)的链接:style-loader文档 基本用法: 用于将 css-loader 打包好的css模块,插入到html文件中,变成一个 <style>标签; 3、file-loader: file-loader文档 基本用法: 用于处理各种资源文件,一般是图片,不然图片是没办法被同时打包的。 2、css-loader配置详解 先吐槽一波,中文文档里的说明,真的是描述的一点都不清楚。 2.1、root 名称 类型 默认值 描述 root {String} / 解析 URL 的路径,以 / 开头的 URL 不会被转译 官方文档里对这个解释不够严谨。 首先,假如不设置设个属性,如果理解为,以 / 开头的url不会被转译,从结果来看,也不算错; 然而,假如设置这个属性的话,那么就不一样了。 在面对图片路径时,这个属性有三种情况: 当不设置这个属性的时候,css-loader不会去解析以/开头的图片路径,也不会报错; 当设置这个属性的时候,即使你设置其值为默认值 /,css-loader也会去尝试解析这个路径,如果找不到对应的图片,会报错; 当设置这个属性的值为非默认值,和【2】中的行为是一样的,css-loader去尝试解析这个路径,如果找到图片,则正常解析,找不到,会报错; 当设置这个属性时,是指,当url以 /为开头时,到底去找哪里的文件夹作为解析以 /为开头的url路径的文件; 当面对css文件路径时,即在css文件里,通过 @import 引入css文件时,这个是不对css文件的路径生效的(即使找不到,也不会报错)。 示例: 文件树: 根目录 |-- src | |-- app.js | |-- src | |-- logo.png | |-- static | |-- abc.png | |-- webpack.config.js 那么在 webpack.config.js 里配置的时候,应该这么写:root: __dirname + '/static/'。 __dirname 表示根目录的绝对路径。假如根目录的路径是 D:/abc/def,那么 __dirname 就表示 D:/abc/def ,而 __dirname + '/static/ 则表示 D:/abc/def/static 这就是告诉 css-loader ,遇见 / 开头的url路径,你应该去 D:/abc/def/static 这个路径下去找文件。 2.2、 名称 类型 默认值 描述 url {Boolean} true 启用/禁用 url() 处理 首先,我们已知,css-loader 正常会解析css属性里的图片url路径,例如 background: url('/logo.png') 里面的值。 那么,假如某图片不在你的工程里,而是在服务器上。 而你是可以预知打包后的html文件和这个图片的相对路径关系,你就可以直接写那个时候的路径,并将url设置为false。 但是,如果设置为false,那么所有url都不会进行转义了(也不会触发file-loader),自然也不会报错(即使图片不存在)。 示例: 假如打包后,上传到服务器的目录为: dist |-- app.js |-- logo.png 那么你如果想引用 logo.png ,那么把 url 设置为 false 之后,然后路径这么写就行了 background: url('./logo.png')。 2.3、alias 名称 类型 默认值 描述 alias {Object} {} 创建别名更容易导入一些模块 说实话我自己捣鼓了半天也没彻底搞明白其原理,但是琢磨出來一些用法: 1、对图片路径生效 假如文件结构: 根目录 |--static | |-- logo.png |-- webpack.config.js 解释: 已知: 图片放在 /static 目录下; 已知:不确认css文件放在哪里(因为模块化,方便移动,所以可能更改模块的目录结构); 需求:我想要确保我的css文件必然能引用到这个图片,即使更改模块的文件路径,也不影响(不需要我二次去修改); 行动:那么添加 css-loader 的属性,设置如下:alias: {'@': __dirname + '/static/'} ; 行动:在css文件里,图片如下引用 background: url(~@/logo.png); 结果:我就可以确保必然css文件必然能引用到这个图片了; 注意: @ 前要加 ~ 让 webpack 识别(~ 是 webpack 负责识别,认为是根目录,而 @ 是 css-loader 负责); 2、对 @import 引入的css文件无效; 假如文件结构: 根目录 |--static | |-- style.css | |-- foo.css |-- webpack.config.js 解释: 文件目录结构如上; 在 style.css 如果通过 @import '~@/foo.css' 来导入; 即使在 webpack.config.js 里这么设置 alias: {'@': __dirname + '/src/style/'} 也是没有用的; 3、解决场景: 这个可以应用的场景挺多,不过现在很多是通过webpack的别名通用配置来解决 css文件和图片文件分离; 也可以分类摆放图片(例如@开头的是风景类图片,peopel开头的是人物图片); 记得在别名之前加一个波浪线~让webpack识别,否则无法正常工作; 2.4、import 名称 类型 默认值 描述 import {Boolean} true 启用/禁用 @import 处理 假如你通过@import导入的是某个打包后工程所在位置的css文件; 即该文件不在打包前的工程里(例如CDN); 那么这个就有用; 表现效果@import导进来的css没有被打包,只是单纯的引入了(该@import代码被直接放在style标签里); 你可以查看dist/index.html的style标签来深刻了解; 这里给一个简单的示例: webpack打包前: // foo.css @import 'http://abc.com/m.css' webpack打包后: // html文件(假设使用了style-loader把css通过style标签插入) <style> @import 'http://abc.com/m.css' </style> 2.5、minimize 名称 类型 默认值 描述 minimize {Boolean|Object} false 启用/禁用 压缩 这个很好理解,原本写css文件的时候,我们各种换行空格,这个改为true之后,换行和空格就去掉了; 一般开发的时候,取环境变量,当环境变量为生产环境的时候,设置为true;开发环境的时候,设置为false; 关于生产环境的配置,可以查看参考示例,搜索 minimize 即可; 压缩前代码: * { margin: 0; border: 0; padding: 0; } .box { border-radius: 150px; } 压缩后代码: *{margin:0;border:0;padding:0}.box{border-radius:150px} 2.6、sourceMap 名称 类型 默认值 描述 sourceMap {Boolean} false 启用/禁用 Sourcemap 在 minimize 设置为 true 后,css代码被压缩了,那么如果我们要调试的话就很麻烦; 当 sourceMap 设置为 true 后,通过Chrome控制台的 Sources 标签,在左边栏上面选 Sources ,可以在树结构的 (no domain) 里,查看到压缩后和压缩前的CSS代码; 即使 minimize 没有设置为 true (不压缩),由于css代码被扔到了js里,因此也是无法直接查看我们写的css代码的; 但是 sourceMap 设置为true后,就可以通过【2】中描述的途径来查看我们写的css代码; 启用 sourceMap 压缩前的代码: 启用 sourceMap 压缩后的代码: 2.7、importLoaders 名称 类型 默认值 描述 importLoaders {Number} {Number} 在 css-loader 前应用的 loader 的数量 说实话,加不加这个,感觉没啥区别(我还专门研究了一波postcss和autoprefixer让他生效。 见我关于postcss的配置,为了正常运行,我在项目里webpack把这个注释掉了,可以取消掉注释 // importLoaders: 0 // 感觉没什么用 如果有见解,欢迎指正 2.8、modules等 名称 类型 默认值 描述 说明 modules {Boolean} false 启用/禁用 CSS 模块 1、初步理解:这个相当于把css视为模块。例如我有一个css文件 foo.css ,然后里面有一个类 .bar,我可以在js文件里通过 import foo from './foo.css'导入这个css文件; 2、在打包后,foo.css里的类 .bar 会变成具有唯一性的一个字符串(举个例子假设他变成了abcdefg); 3、假如我在html里使用的是class='bar',那么显然是无法正常生效的(bar被转为了abcdefg); 4、那么我可以使用变量foo.bar(在js这里,这是一个变量),赋给原本使用class='bar'的这个DOM节点; 5、由于是变量,所以他的值事实上已经被css-loader转为了abcdefg,可以正常运行了; 6、推荐阮一峰的博客CSS Modules 用法教程 camelCase {Boolean|String} false 以驼峰化式命名导出类名 1、这个需要结合modules来看,在modules设置为true时,我们可以通过变量名来获取更名后的css类名; 2、但我们写css类名的时候,一般是例如foo-bar这种写法,在js里显然不合适; 3、因此把这个启用为true,我们就可以使用fooBar这种驼峰式写法了,方便js引用; localIdentName {String} [hash:base64] 配置生成的标识符(ident) 1、这个也是跟modules相关的,用于对原本混算复杂不具有可读性的类名,进行重命名; 2、我觉得这个文章讲这个比较好,你真的知道 css-loader 怎么用吗?,搜索关键词:localIdentName 这三个是一起使用的的,见表格内容吧。 3、项目地址 https://github.com/qq20004604/webpack-study/tree/master/5%E3%80%81Loader/css_loader ,请 Star 和 fork 到本地后,注意相关配置。
前注: 文档全文请查看 根目录的文档说明。 如果可以,请给本项目加【Star】和【Fork】持续关注。 有疑义请点击这里,发【Issues】。 5.1、babel-loader 这个用于将使用ES6规范的js代码,转为ES5。 首先安装一大堆东西,参照下面的命令,一共是4个(包括webpack) npm install --save babel-loader babel-core babel-preset-env webpack 创建babel规则文件.babelrc,内容设置为: { "presets": [ [ "env", { "modules": false, "targets": { "browsers": [ "> 1%", "last 2 versions", "not ie <= 8" ] } } ] ] } 然后app.js里添加文件内容(这显然是es6语法): let foo = () => { console.log('1') } foo() 运行 npm run test 执行脚本,等脚本执行完毕后,查看dist文件夹下的 dist.js 文件。 会发现代码已经被成功转为非es6语法了(截取如下): var foo = function foo() { console.log('1'); }; foo(); 但是,这个只能转一些普通的es6语法,像例如Promise、Set之类的,他是无法转换的。 如果想要转换这些,我们需要做一些额外的工作。 首先安装插件 npm install babel-runtime --save npm install babel-plugin-transform-runtime --save-dev 然后修改.babelrc文件的内容为: { "presets": [ "babel-preset-env" ], "plugins": [ "transform-runtime" ] } 【注】: babel-runtime(也就是上面plugins数组中的transform-runtime),解决了辅助代码(即让我们可以使用新特性的代码)被默认添加到每一个需要他的文件的问题(这会导致文件过大)。 具体解决方法是禁用了babel对每个文件的runtime注入,引入 babel-plugin-transform-runtime 并且使所有辅助代码从这里引用。 表现效果:假如A模块异步加载B模块,A、B模块里都使用了Set,那么为了使A模块正常运行,引入了某些代码。然后因为B模块又是被A模块引入的,那么B模块在被加载的时候,A模块里已经引入的代码,就没必要再次引入了,所以B模块里是不存在A模块引入的那些兼容代码的。 【注(完)】 修改webpack设置文件的loader内容为: { test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader' } 最后修改app.js这个文件的内容,给里面加一些特殊的es6语法: let foo = () => { console.log('1') } foo() let bar = new Promise((resolve, reject) => { resolve(1) }) bar.then(msg => console.log(msg)) let baz = new Set([1, 2, 3]) console.log(baz) let another = async function () { console.time('timeout') let result = await new Promise((resolve, reject) => { console.log('in Promise') setTimeout(() => { resolve('Promise resolve') }, 1000) }) console.log(result) console.timeEnd('timeEnd') } another() 以上代码包含es6的Promise,Set,以及es7中的async/await。 此时我们运行一下npm run test试试,然后查看dist/dist.js文件,会发现我们的代码出现在大约1040行的位置,并且原本使用es6、es7语法的代码,都被一段很长很复杂的代码所替换(因为太长,所以这里略过)。 这说明我们转义成功了! 更多请参照【实战3】解决有es6、es7语法的js代码
前注: 文档全文请查看 根目录的文档说明。 如果可以,请给本项目加【Star】和【Fork】持续关注。 有疑义请点击这里,发【Issues】。 4、出口 示例目录 4.1、标准的出口写法 // 出口文件 output: { filename: './dist/dist.js' } 意思是,将打包好的文件,打包到dist文件夹下的dist.js。 注: 大家一般将打包好的文件会放在dist文件夹下,方便管理。 4.2、出口文件名根据入口文件名所决定: 上面讲了多入口,以及对应的多出口的配置写法,可以参考上面【3】中的内容。 那么假如单入口entry: './app.js',,然后output直接写filename: './dist/[name].js'会发生什么事情呢? 因为入口相当于main: './app.js',所以打包好的文件名是:main.js。 4.3、设置出口目录 之前我们文件名是统一写的,但在某些情况下(比如根据环境变量来决定出口目录,因此可能存在多个出口目录,在不同情况下【生产/测试】输出到不同目录)。 因此设置方法如下: output: { path: __dirname + '/dist', filename: 'dist.js' } __dirname是一个绝对路径,指从根目录到当前目录的路径(不带最后一个/),因此path: __dirname + '/dist'指以绝对路径写的到当前目录的dist文件夹下。 注意,这个情况下,path不能用相对路径(如./dist来写),必须写成绝对路径。 而filename就是指输出到该绝对路径下,打包好的文件的名字(参考之前的,是一样的)。 4.4、占位符 在上面,解决多入口文件名的问题时,我们使用了[name]来根据入口文件自动生成文件名。 除了[name]之外,我们往往需要给文件名增加[hash]值来解决缓存的问题(即代码更新后,由于文件名的不同,强制用户下载最新的代码)。 增加方法如下: ... filename: 'dist.[hash].js' ... 原本打包后的文件名为:dist.js,现如今打包后的文件名为(示例):dist.5099da45ae9fc763852d.js。 如果要限制hash值的长度,可以通过[hash:10]来限制长度(默认是20,这里输出10位)。输出文件名示例为:dist.49b3713789.js 注:使用[hash]时,这里的hash值,即使文件没有改变,每次生成的结果也不同。 如果想让模块没有改变时,hash值不改变,那么应该使用[chunkhash]替代[hash]。 chunk表示模块,chunkhash就是指根据模块内容计算出来的哈希值。 还有一些其他的占位符,以下表格,是我根据官方文档追加写的占位符说明: 模板 描述 特点 [hash] 模块标识符(module identifier)的 hash 每次都不同(低版本webpack可能有问题) [chunkhash] chunk 内容的 hash 模块内容不变,hash值不变(不能和hash同时使用) [name] 模块名称 就是entry的key,单入口缩写写法默认是main [id] 模块标识符(module identifier) 默认情况下是例如'0','1'之类 [query] 模块的 query,例如,文件名 ? 后面的字符串 我也没搞懂这个 因此一个示例是: filename: 'dist.chunkhash=[chunkhash:10].name=[name].id=[id].js' 具体效果请fork我的项目,并down下来到本地; 然后进入【4、出口文件夹】,在该目录下运行npm run test; 最后查看dist文件夹里生成的js文件。对比webpack.config.js文件中的output属性,对比之。 至于加了哈希值后的文件,如何让html自动引入,下来进行说明。 4.5、引入启用了占位符的打好包文件 在【4.4】中,我们启用了 [hash] 和 [chunkhash] 占位符。 这个占位符,会根据哈希值,在打包好的js文件的文件名中,添加一段hash值。 而这个hash值显然是不可预期的,如果我们每次都在html里手动去写这些js文件名,不仅傻,还容易漏和犯错。 因此我们需要设法解决这个问题。 解决步骤: webpack不能全局安装(虽然也可以,但是会造成污染),因此我们先在当前文件夹下安装一次webpack:npm install --save webpack; 我们还需要安装一个webpack插件:npm install --save-dev html-webpack-plugin; 除此之外,我们需要配置一下webpack文件。做两件事:1、引入插件;2、配置插件; // webpack.config.js // 引入插件 const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { // 入口文件,指向app.js entry: './app.js', // 出口文件 output: { path: __dirname + '/dist', filename: 'dist.chunkhash=[chunkhash:10].name=[name].id=[id].js' }, // 将插件添加到webpack中 plugins: [ // 这里是添加的插件 new HtmlWebpackPlugin({ title: 'My HTML' }) ] } 最后,如之前一样,运行npm run test,会发现在dist文件夹下,除了之前的js文件,还出现了一个html文件,而这个html文件引入了我们打包好的js文件。 注: 项目里的是已经将依赖加入了package.json,直接运行npm install即可自动安装webpack和该插件。 4.6、同时引入固定资源,以及打包好的文件 在【4.5】中,我们启用了插件,让插件可以自动创建html模板,并让该html文件引入打包好的js文件。 在本章,我们不会深入讲解这个插件,但是需要解决一个常见需求: 我通过CDN引入jQuery(或其他类似资源); 并且该资源可能是一个,或者多个; 或者是其他已经写在html里的文件内容; 我不想在自动打包好html后,再去手动插入script标签或者其他类似标签; 因此我希望以某个html文件为模板,额外加入打包好的js文件; 因此我们需要对这个插件进行配置:HtmlWebpackPlugin的文档(英文) 对于这个需求,我们只需要配置一些简单的东西: ···plugins: [ // 这里是添加的插件 new HtmlWebpackPlugin({ title: 'title', // html的title(就是title标签里的东西) filename: 'index.html', // 重写后的html文件名,默认是index.html template: './demo.html', // 这个就是那个模板文件,不会改动原有的内容,而是在原来html文件的末尾,将打包编译好的文件添加进去 }) ]··· 然后在模板文件里添加一些内容(具体查看文件夹内的 demo.html 文件。 最后一如既往的运行npm run test即可,查看 dist 文件夹下的 index.html 文件。
前注: 文档全文请查看 根目录的文档说明。 如果可以,请给本项目加【Star】和【Fork】持续关注。 有疑义请点击这里,发【Issues】。 3、入口(多入口) 示例目录 在上面的webpack.config.js中,有如下代码: // 入口文件,指向app.js entry: './app.js', 以上代码相当于: entry: { main: './app.js' } 如果是普通的项目(单入口),那么按照上面的方式写(entry: './app.js')就可以了。 至于下面的方式是什么呢?答案是:用于提供【多入口】的解决方案。 假如我一个项目里,允许有A、B两个html文件,他们之间是不同的入口文件(比如一个是用户入口页,一个是管理入口页)。 显然虽然是两个不同的入口,但是他们之间有很多共通的逻辑(否则就有大量重复开发工作了),因此我们需要将其写在同一个工程中,然后通过不同的入口文件引入他。 他的依赖树可能是这样的: . |____first.html | |____first.js | | |____common.js |____second.html | |____second.js | | |____common.js 也就是说,first.js和second.js两个文件,都共享一个common.js模块。 如示例代码点击查看github。 核心代码如下: // webpack.config.js ... entry: { first: './first_entry.js', second: './second_entry.js' }, ... 当然,只配置入口,是无法正常运行的,会报错: Multiple assets emit to the same filename 意思就是,你把多入口文件打包到一个文件里了,这样是不对的。 因此我们应当这样配置: output: { // 文件名,将打包好的导出为bundle.js filename: './dist/[name].js' } 这段代码的意思是: 将多入口文件,打包到dist文件夹下; 并且名字根据入口文件决定; [name]表示文件名自动匹配入口文件的key(即first: './first_entry.js'里面的first); fork本项目,并且在本文件夹下执行npm run test来打包,然后打开first.html和second.html来查看效果(见控制台console)
前注: 文档全文请查看 根目录的文档说明。 如果可以,请给本项目加【Star】和【Fork】持续关注。 有疑义请点击这里,发【Issues】。 2、简单指令(npm脚本) 示例目录 我们实际开发中,一般都是使用npm run build或者npm run dev之类的指令,这是怎么实现的呢? 答案是利用package.json里面的scripts属性。 其他文件如【1】中的四个文件,新增一个package.json,内容如下: // package.json 注:name只能是以下这种格式,不能有空格或者中文 { "name": "simple-command", "version": "0.0.1", "scripts": { "test": "webpack --config webpack.config.js" } } 然后控制台执行命令npm run test即可。 注: 之所以我们能通过npm run test来执行"webpack --config webpack.config.js"这样一段命令。 原因是这段命令的开头,以npm为开头,所以执行的是全局变量(通常是全局变量,因为npm一般是全局安装)配置的npm包管理器。 然后后面的run test是npm负责去执行的,所以npm run test这段命令,是npm的特性,而不是webpack的,称作npm脚本。 而之后webpack的命令,是webpack做的事情。但webpack的执行,显然是通过Node.js执行的,所以可以用JavaScript语法。 npm小结(程序猿小卡) npm的工作原理
0、前注 本文内容源于webpack中文文档,以及我自己实践中写的若干DEMO。 每个DEMO以文件夹为单位,从入门到进阶,根据文件夹编号为准,逐步递进。 成文时,webpack版本是【3.8.1】 0.1、安装webpack 首先你需要安装Node.js,点击打开Node.js下载页面。安装完Node.js后,会自带npm包管理器。 npm install webpack -g 这个命令将安装最新版本的webpack(全局,学习教程中推荐,避免多次安装。但实践中还是有必要一个项目一个webpack,避免版本冲突带来的bug) 目前版本是3.8.1(2017/11/27) webpack -v 查看当前webpack版本 执行命令: 以下执行webpack命令时,指在对应文件夹下,通过控制台执行命令。 快速抵达对应目录的控制台(win): 在对应目录下,按住 shift,然后点击鼠标右键,在弹窗里选择在此处打开命令窗口即可启用 1、webpack基本结构 示例目录 文件目录见1、最简单的webpack实例这个目录。 // webpack.config.js 这个是webpack的管理配置文件 // 以CMD的格式导出模块 module.exports = { // 入口文件,指向app.js entry: './app.js', // 出口文件 output: { // 文件名,将打包好的导出为bundle.js filename: './bundle.js' } } // app.js 这个是入口文件 import bar from './bar' bar() // bar.js 这个是入口文件引入的模块 export default function bar () { console.log('bar') } // page.html 这个是html目录文件,这个文件引入入口文件 <html> <head> <title>1、最简单的webpack实例</title> </head> <body> <script src="./bundle.js"></script> </body> </html> 控制台执行webpack(或者 webpack --config webpack.config.js ),会显示如下内容: D:\study notes\Project\webpack_learner\1、最简单的webpack实例>webpack Hash: 2fdcc03878d7c5480ce6 Version: webpack 3.8.1 Time: 58ms Asset Size Chunks Chunk Names ./bundle.js 3.13 kB 0 [emitted] main [0] ./app.js 115 bytes {0} [built] [1] ./bar.js 142 bytes {0} [built] 打完后的bundle.js文件内容略。这个时候打开html文件,查看控制台,会发现正常输出了bar。
1、安装gulp gulp官网 gulp的安装 核心点是gulp文件的文件名一定是:gulpfile.js。 task就是一个任务(要做的一系列事) 运行通过gulp来执行默认的task或者通过gulp task名来执行指定的task(因为一个gulp文件里可能有多个互相独立的task 2、读取文件 gulp.src(files[, options]) 效果: 读取文件,产生数据流。 files的写法(字符串或数组)(必填): js/app.js:指定确切的文件名; js/*.js:某个目录所有后缀名为js的文件; js/**/*.js:某个目录及其所有子目录中的所有后缀名为js的文件; !js/app.js:除了js/app.js以外的所有文件。 *.+(js css):匹配项目根目录下,所有后缀名为js或css的文件。 files的写法(对象)(选填): 官方文档 1、options.buffer 默认是true,以buffer的形式读取(即一次读取整个文件),而改为false的时候则为stream(流)的方式读取。 流模式适合读取大文件,但是一般的html、css、js之类的,可以用buffer读取(但推荐用流)。 假如你需要读取完整个文件,然后对整个文件正则匹配,那么只能用buffer的形式。 2、options.read 默认true,设为false则file.contents返回值为null(不会读取文件) 还有 options.base 以及 node-glob 和 glob-stream 所支持的参数,但是这里略过。 示例: var gulp = require('gulp'); gulp.task('default', function () { gulp.src('a.js') }); 就是这么简单,读取了一个文件 3、拿来一个流,做点事,再把他返回 stream.pipe(fn) 简单来说,通过gulp.src(),我们已经读取了一个文件流,然而我们需要对这个文件流做点事,那么就是pipe的作用了。 1、获取文件流:pipe函数用于处理文件流(来源于上下文),即调用pipe方法的这个对象; 2、处理文件流:pipe接受一个参数,这个参数用于处理这个文件流; 3、返回文件流:这个处理文件流的参数,最后要返回处理后的这个流; 4、连写pipe:因为拿来和最后返回的是同一个东西,因此是可以连写的(就像jQuery选择器选择到DOM后的连写那样); 先给一个简单的示例吧: var gulp = require('gulp'); var through = require('through2'); gulp.task('default', function () { gulp.src('a.js') .pipe(through.obj(function (file, encode, cb) { console.log('第一次处理') this.push(file) cb() })) .pipe(through.obj(function (file, encode, cb) { console.log('第二次处理') this.push(file) cb() })) }); // 输出 第一次处理 第二次处理 4、对这个文件流做点啥 上面只是简述了pipe干嘛用的,那么现在我们实际用文件流的形式做点什么。 through2模块,用于处理文件流 这个模块干嘛用的,有兴趣的可以看看npm里的through2这个模块,知乎的回答 用这个模块,基本套路很简单: 1、引入 through2 模块; 2、调用他的obj方法,并传一个函数作为参数(这个函数是我们的处理函数);.pipe(through.obj(callback)) 3、写这个callback处理函数; 4、这个callback有三个参数,分别是:file,encode(文件编码,比如'utf8'),cb(继续执行,类似 express 里路由的 next); 5、我们先对 file 干点啥,然后通过 this.push(file)(这里的file是修改后file)才能继续下面的 pipe,最后执行 cb() 继续下一个 pipe。 基本示例(不对file做点什么): var gulp = require('gulp'); var through = require('through2'); gulp.task('default', function () { gulp.src('a.js') .pipe(through.obj(function (file, encode, cb) { console.log(arguments) this.push(file) cb() })) }); // 输出结果 { '0': <File "a.js" <Buffer 61 62 63 64 65 66>>, '1': 'utf8', '2': [Function] } 再给一个在原文件内容后拼接了一个字符串的DEMO: var gulp = require('gulp'); var through = require('through2'); gulp.task('default', function () { gulp.src('a.js') .pipe(through.obj(function (file, encode, cb) { // 显示当前的文本内容 console.log(file.contents.toString()) // 文本内容转为字符串 var result = file.contents.toString() // 添加了一点东西 result += ' => I add some words here' // 再次转为Buffer对象,并赋值给文件内容 file.contents = new Buffer(result) // 以下是例行公事 this.push(file) cb() })) .pipe(through.obj(function (file, encode, cb) { // 显示当前的文本内容(这次显示的是修改后的) console.log(file.contents.toString()) this.push(file) cb() })) // 把文件写到一个新的文件夹里(不影响原有的),目录是modified-files .pipe(gulp.dest('modified-files')); }); // 输出 abcdef abcdef => I add some words here 并且modified-files/a.js文件里的内容是修改后的内容 讲道理说,懂以上方法,已经可以解决很多问题了。 无非就是读取文件,转为字符串,改改改,变为Buffer对象赋值回去,写到一个新的文件夹里(原文件名不变)
1、前注 关于es6的详细说明,可以参照我的系列文章es6 从入门到熟练,或者阮一峰的ECMAScript 6 入门。 我的系列文章,是在阮一峰的基础上,增加了更多适合初中级开发者的内容(包括大量的示例代码和解释),以降低学习难度,丰富说明。 本文是对es6整体的回顾,结合我的实际开发经验,对es6的一个小结。 为了精炼内容,es6里不常用的内容已经去掉,而对常用、重要的es6知识,附上简单的代码说明,并另附有详细说明的博文链接,方便初中级开发者理解。 2、开发环境 关键字:IE9、Babel、Babel的垫片、脚手架 首先,使用es6的前提是最低IE9,如果你需要兼容IE8,建议放弃es6,专心使用神器jQuery。 其次,如果需要使用es6来编写,那么你需要Babel转码器用于将你的es6代码转换为es5代码,用于兼容只能使用es5的环境。否则对于只能运行es5的环境(例如IE9),是无法运行es6代码的。 第三,由于Babel在默认情况下,并不是全部转换的,如以下说明: Babel 默认只转换新的 JavaScript 句法(syntax),而不转换新的 API,比如Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise等全局对象,以及一些定义在全局对象上的方法(比如Object.assign)都不会转码。 因此,我们需要垫片,一般情况下可以用babel-polyfill,也可以用babel-runtime,这两个有所差异。 babel-polyfill会污染全局对象,即会对例如Object这样的对象添加方法,而babel-runtime只会转换es6语法的代码,但如果你需要调用Object.assign这样的方法,那就不行了。 由于细节很多,因此这里给几个参考链接吧: Babel 全家桶 babel的polyfill和runtime的区别 看完以上两个,可能会觉得应该同时使用这两个,然而并不需要,看下面这个链接: transform-runtime 会自动应用 polyfill,即便没有使用 babel-polyfill的conanliu于17 Dec 2016提交的issues。 如果你使用的Vue.js,那么可以直接fork我的脚手架,然后当做自己的脚手架使用。 附脚手架链接:vue-scaffold,如果可以,给个star喔~~ 如果你用的不是Vue.js,那么可以去搜一下你所使用的框架的脚手架,然后拿来使用。如果找不到,可以找使用带脚手架的该框架项目,然后down下来,删除对方的项目只取壳来用即可。(如果有许可,记得阅读一下许可看能不能这么干) 3、let和const 既然有let和const了,那么推荐优先使用这两个。 一般情况下,let可以直接替代var,对于常量,可以用const。 这不是必须的,但用这2个可以帮你规范写代码的习惯,所以还是强烈推荐的。 比较蛋疼的是,用webtorm,let有时候不会高亮,只有var和const是高亮的。这个可能是用的风格的问题,我也不太确定。解决方案我自己是没找到,凑合用吧。 另外,let和var之间的一个重要区别是变量提升,所以如果你写代码不太规范的话,可能会报错,好好检查一下吧。 另外,阮一峰推荐将函数设置为常量,就像这样子: const add = function (a, b) { return a + b } 我觉得挺有道理的,推荐。 4、字符串 既然用es6,当然要用反引号这个高大上的东西了。 详细用法推荐我自己的博客:ECMAScript 6(7)模板字符串 最基本的用法,可以直接用反引号替代普通的引号(单引号和双引号) 例如: let a = 'ab' // 可以直接用以下替换 let a = `ab` 而且一般情况下,简单需求不用再拼接字符串了~(另外,反引号也可以像普通字符串那样拼接) 如: let str = '20004604'; let html = 'my QQ is ' + str; //用以下替换 let str = '20004604'; let html = `my QQ is ${str}`; 简单暴力省事。 5、解构赋值 最大的好处是简化了写法,如代码: let obj = { a: 1, b: 2 } //old let a = obj.a; let b = obj.b; // es6 let {a, b} = obj 除了对象之外,还有数组也可以解构赋值,别忘了。 6、对象 es6的对象,比早期版本的写起来舒服很多。 例如: 对象属性是函数的时候可以简写; setter和getter的简写; 通过Object.assign()来合并对象,实现继承或添加属性效果; 可以用属性名表达式; 可以用变量名只要作为对象的属性名,并且变量的值可以自动成为对象该属性名的值; 列一些常见写法: let obj = { // 对象属性是函数的时候可以简写 a(){ console.log('对象属性是函数的时候可以简写') }, // setter和getter的简写; get b() { return this._b }, set b(val) { this._b = val } } let c = '添加了一个c' // 通过``Object.assign()``来合并对象,实现继承或添加属性效果 // 可以用变量名只要作为对象的属性名,并且变量的值可以自动成为对象该属性名的值 Object.assign(obj, { c }) // 可以用属性名表达式 let d = "abcd" obj[d.replace(/abc/, '')] = '属性名表达式' 7、数组 最常用的就两个: 扩展运算符...; 将类数组的转为数组的Array.from() 如代码: function getArgs() { let foo = [...arguments] console.log(foo) let bar = Array.from(arguments) console.log(bar) } getArgs(1, 2, 3) // [1, 2, 3] // [1, 2, 3] 需要注意的一个特性: es5在面对,通过Array(5)这样生成带空位的数组时,处理他的时候会跳过空位数组的空位; es6在同样情况下,因为使用遍历器接口,所以会进行处理(视为undefined),而不是跳过; 8、函数 函数常用特性有以下几个: 箭头函数:特点是this永远指向声明时的父级作用域,写起来比普通函数简单; bind:可以给函数绑定this,并将这个绑定后的函数返回(不影响原函数); rest函数:即函数参数使用例如function test(..args){}这样的,这个返回的是一个数组,而不是类数组。 参数默认值:一般带默认值的参数,放在参数列表的后面。 function test(a, b = 3) { console.log(a, b) console.log(this) } test.bind('Is this')(1) // 1 3 // Is this function test2(...args) { console.log(args.length) } test2(1, 2, 3, 4, 5) // 5 9、Set和Map Set结构最大的特点是去重,Map结构最大的特点是kv结构。 Set: Set和数组类似,可以存储元素,但是Set不能存储相同的值。 非引用类型变量来说,就是值相等;对于引用类型变量来说,指地址相等(而不是值相等)。详细情况请点击Set类型和WeakSet查看。 至于去重,一般是对数组使用。先作为参数生成一个Set类型变量,再利用扩展运算符变回数组,去重完成,完美。 利用扩展运算符,调用Set的迭代器接口 // 去重 let foo = new Set([1, 2, 3, 3, 3]) console.log([...foo]); // [1, 2, 3] Map: Map结构和对象非常类似,不过最大的区别在于,Map结构可以用其他类型作为key,例如数组、对象等。 Map可以参照这篇博客Map和WeakMap 示例代码: let zhang = { firstName: "王" } let property = { gender: "男" } let foo = new Map() foo.set(zhang, property) foo.has(zhang) // true foo.get(zhang) // {gender: "男"} 10、Promise Promise是es6的精华之一,他非常适用于异步处理。 Promise对象在使用的时候,分为两部分,第一部分是new Promise这一步,第二部分是对返回的Promise实例进行处理的内容。 因为是通过执行resolve或reject来改变Promise的状态,从而决定执行then的时机的(类似回调函数),以及执行的哪一个。因此写起来和回调函数相近,但是可以连写,避免回调地狱的情况。 关于Promise的详细介绍请阅读Promise(1)基础知识及之后三篇博客 如示例代码(对比普通ajax和promise)(另注:为了方便理解,仿jQuery的写法,并且没有用jQuery的$.ajax().then()这种写法) // 模拟ajax function ajax (options) { setTimeout(function () { options.success(options.url) }, 1000) } // old let foo = function (callback) { ajax({ url: "/1", success: function (result) { callback(result) } }) } let foo2 = function (result) { console.log(result) return function (callback) { ajax({ url: "/2", success: function (val) { callback(val) } }) } } // 核心,调用的时候如果是连续请求的话,基本要写成回调地狱了 foo(function (result) { foo2(result)(function (val) { console.log(val) }) }) // Promise let bar = function () { return new Promise((resolve, reject) => { ajax({ url: "/1", success: function (result) { resolve(result) } }) }) } let bar2 = function (result) { console.log(result) return new Promise((resolve, reject) => { ajax({ url: "/2", success: function (val) { resolve(val) } }) }) } // 核心,then连写即可 bar().then(function (result) { return bar2(result) }).then(function (result) { console.log(result) }) 显然,then连写比回调函数的写法要方便一些。 如果面对的是特殊需求,比如是多个ajax请求全部完成后,再执行执行函数,那么Promise的优势会更大一些,而非Promise写法要麻烦很多。 甚至如果要对错误进行处理,那么Promise写法会更方便。 不过这里只是小结,就不细说了。 11、class class是好东西。 有了class后,写构造函数、写类的继承的难度,下降了很多很多。 先附我的博文class(1)基本概念,以及之后5篇博文。 由于很简单,给一个示例大约就能理解这个是怎么用的: class Foo { constructor () { console.log('this is constructor') this.defaultValue = '变量要在构造函数里赋值,而不能直接声明' } log () { console.log('log') } } let foo = new Foo() // this is constructor foo.log() // log foo.defaultValue // "变量要在构造函数里赋值,而不能直接声明" 12、es6模块 es6的模块不同于以往的CommonJS(node用,服务器环境),AMD(RequireJS的规范,浏览器环境,依赖前置)、CMD(SeaJS定义的规范,浏览器环境,依赖就近)。 他的特点有两个: 编译时加载,因此可以做静态优化; 模块的引用进来的,都是值的引用,而非值的拷贝。 缺点是: 浏览器环境下不支持,node环境下支持的也比较差; 必须考babel转码后才可以正常使用,因此对某些符合规范的特性支持的不是很好; 详细说明阅读这篇博客:es6的import和export,另外三个规范阅读这篇博客AMD、CMD、CommonJS 基本使用方式如示例代码: // foo.js let foo = 'foo' export default foo // bar.js import foo from 'foo' console.log(foo) 13、async函数 这个并不是es6的,而是es2017(又称es8)的内容。 可以认为async函数是Generator函数的语法糖,详细说明参照这篇博客:async函数。 他的前置知识比较多,包括Iterator遍历器、Generator状态机、Thunk函数(自动执行Generator 函数)。 简单的说,假如有多个异步请求,你需要让这些起步请求依次执行,例如在执行完前一个之后,再执行后一个。那么你就需要async函数了。 async函数可以让你写这种请求如同写同步函数一样简单(对比【10】中的Promise更简单)。 以下示例是基于【10】中的代码,在最后一步执行的时候,改用async函数来完成 // 模拟ajax function ajax (options) { setTimeout(function () { options.success(options.url) }, 1000) } // Promise let bar = function () { return new Promise((resolve, reject) => { ajax({ url: "/1", success: function (result) { resolve(result) } }) }) } let bar2 = function (result) { console.log(result) return new Promise((resolve, reject) => { ajax({ url: "/2", success: function (val) { resolve(val) } }) }) } async function foo () { let result1 = await bar() let result2 = await bar2(result1) return result2 } foo().then(result => { console.log(result) }) 可以发现,async让连续异步调用像写同步函数一样简单。 14、ESLint 规范化开发,建议还是用ESLint来帮忙检查吧。不会这个怎么行? ESLint是一个语法规则和代码风格的检查工具,可以用来保证写出语法正确、风格统一的代码。 这里直接给一个阮一峰写的文章,等以后我再单独补一篇详细用法的博客。 ESLint的使用 15、小结 es6常用内容基本就以上13点。 虽然es6实际包括了很多知识,例如: string方面增加了对utf16字符的更好的支持; number方面增加了对最大值、最小值、以及合理误差误差值的处理等; Symbol产生唯一变量; Proxy的代理; 遍历器,状态机等等。 但实际常用的就以上这些,如果只是日常使用的话,熟悉以上内容足够了。 但若要做的更好,那么应该深入学习,es6新增的很多内容,是将传统后端语言的一些很好的思想,搬到JavaScript来,让js规范化。 对于专精于前端的同学,学习es6的过程中,可以学习到这些来自于其他语言的精华,因此建议至少完整的看一遍,勿要只满足于常用的这些API。
let 解释: 1. 简单来说,就是类似var,但使用该方法声明的变量,只在当前作用域生效; 几个特点: 1、let和var相比,不存在变量提升(即先使用后声明会报错); { console.log(a); //Uncaught ReferenceError: a is not defined let a = 1; } 2、使用let声明的变量,当前作用域里,该变量唯一。 即,假如在当前作用于的父级作用域里声明一个var a,在当前作用域里也声明一个let a,那么在当前作用域里,只有let声明的a生效,也就是说,以下代码是不可行的: var a = 1; { console.log(a); //Uncaught ReferenceError: a is not defined let a = 2; } 但若将let a改为var a,那么console.log的结果就是1了(采用父级作用域的变量的值); var a = 1; { console.log(a); //1 var a = 2; } 3、同一级作用域里面,只允许对一个变量使用一个let来进行声明。 具体来说,2个var a是正常的,剩下的2个let a或者先let a后var a又或者先var a再let a是统统都是报错的。 { var a = 1; var a = 2; //ok } { var a = 1; //Uncaught SyntaxError: Identifier 'a' has already been declared let a = 2; } { let a = 1; let a = 2; //Uncaught SyntaxError: Identifier 'a' has already been declared } { let a = 1; var a = 2; //Uncaught SyntaxError: Identifier 'a' has already been declared } 4、但是不同级作用域里,是没有影响的,比如分别在父级作用域和当前作用域里声明let a各一次,是没问题的。 另外,外层和内层作用域都声明了一个同样的变量名时,内层作用域该变量的值的修改,对外层作用域的值是没有影响的。 { let a = 1; { let a = 2; //can do it } console.log(a); //1 } 但若内层不适用let声明,而是直接调用a = 2进行修改,那么是有影响的 { let a = 1; { a = 2; //1 to 2 } console.log(a); //2 } 5、let的限制情况 不是十分确认,是实践后的结果。(部分因为条件不足,没法调试) 变量名和和页面里html标签的id名重复时,该变量在全局作用域下。变量声明时不能用let,只能用var(在safari浏览器下会发生此bug); 解决方法是比如把变量名放在局部作用于内; 有时候let需要在"use strict"条件下才能使用(严格模式),我在写nodejs的服务器端遇见过这种问题; 块级作用域 1、let相当于新增了块级作用域。 简单来说,以前只有全局作用域和函数作用域,例如以下是函数作用域的体现: (function () { var a = 1; })(); console.log(a); //Uncaught SyntaxError: Unexpected identifier 而在使用var时,是不存在块级作用域的,即如下代码视为同一个作用域内,所以console.log可以显示结果: { var a = 1; } console.log(a); //1 而使用let时,会相当于创造出了一个块级作用域,例如将以上代码改用let进行声明,则在块级作用域外无法正常显示结果: { let a = 1; } console.log(a); //Uncaught ReferenceError: a is not defined 【Babel处理】另外提一句,使用babel在对以上代码进行转换处理时,为了使结果符合预期的运行结果,他会自动进行一些处理。 例如将let a转换为其他的,例如var _a,而下面的console.log(a)不变;如果有重名(比如有_a)他会继续处理,变为_a2这样。 2、在块级作用域下进行函数声明 简单来说,let制造了块级作用域(见上面); 而ES5中(也许还包括更早的),理论上,函数只能在全局作用域和函数作用域内声明,不能在块级作用域内声明。但实际上,因为要兼容以前版本,所以是可以的(不会出错,除非在严格模式'use strict')。 但也是因为这样,所以如果在块级作用域内声明函数,你很难控制在不同浏览器中(包括同一浏览器不同版本)的实现是同样的效果。所以应该 尽量避免在块级作用域内声明函数。 3、do表达式(存疑) 按照阮一峰的博客关于do表达式的说明,现在有一个提案,使得块级作用域可以变为表达式,也就是说可以返回值,办法就是在块级作用域之前加上do,使它变为do表达式。 代码如下: let x = do { //Uncaught SyntaxError: Unexpected token do let t = f(); t * t + 1; }; 我实测无效(chrome版本 55.0.2883.87),会报错,报错信息见注释,不知为何。也许是该提案未实现? const 解释: 1. 简单来说,学过c++的可以理解为c++的const,没学过可以继续往下看; 2. 如果指向非按引用传递类型(比如字符串,布尔值等),那么该值声明后无法被修改; 3. 如果指向按引用传递,则无法更改其指向的对象,但该对象的值可以被修改; 4. 准确的说,是让按引用传递时,保证该const变量指向的地址不变(而非该地址里的数据不变)(理解本条需要有指针相关概念); 1、指向非按引用传递类型的变量,其变量值不可以被修改 即声明后不能被修改,修改会报错; const a = 1; a = 2; //Uncaught TypeError: Assignment to constant variable. 2、指向引用类型的变量,其值可以被修改,但是不能让其指向另外一个对象 对象的值可以被修改: const a = {test: 1}; a.test = 2; console.log(a.test); //2 不能修改指向的对象:(报错这步是因为更改了指向的对象) var a = {test: 1}; var b = {another: 2}; const c = a; console.log(c); //{test:1} c = b; //Uncaught TypeError: Assignment to constant variable. 3、不能声明const变量时不赋值 会报错 const a; //Uncaught SyntaxError: Missing initializer in const declaration 4、块级作用域,相关特性类似let 显然是块级的 var a = 1; { const a = 2; console.log(a); //2 } console.log(a); //1 不存在变量提升,出现暂时性死区,不能先使用后声明 { console.log(a); //Uncaught ReferenceError: a is not defined const a = 1; } 也不可重复声明(在同一个块级作用域内)(使用let和var同样不可) { const a = 1; const a = 2; //Uncaught SyntaxError: Identifier 'a' has already been declared } 5、指向一个被冻结的对象 const和Object.freeze不同,后者是冻结对象,而前者只涉及地址。 所以可以二者结合起来,让const变量指向一个被冻结的对象。那么该变量则不可更改指向的目标(因为const)也不可更改其值(因为冻结)。 先从阮一峰的博客拿来一个深度冻结函数(递归冻结该对象所有属性): var constantize = (obj) => { Object.freeze(obj); Object.keys(obj).forEach((key, value) => { if (typeof obj[key] === 'object') { constantize(obj[key]); } }); }; 然后略微修改,让const变量指向一个被冻结的对象, 会发现既无法更改变量里对象的值,也无法让变量指向另外一个对象。 有点像让const变量成为一个常量。 (下面代码没有体现深度冻结的效果) var constantize = (obj) => { Object.freeze(obj); Object.keys(obj).forEach((key, value) => { if (typeof obj[key] === 'object') { constantize(obj[key]); } }); return obj; //I add this code }; const a = constantize({a: 1}); console.log(a); //{a:1} a.a = 2; console.log(a.a); //1 a = 10; //Uncaught TypeError: Assignment to constant variable. 顶层对象的属性 所谓顶层对象,在js里面指window 当一个变量在顶层作用域里(比如说打开浏览器通过F12的console来直接输入命令),那么该变量在之前情况下,是属于window这个顶层对象的属性的; 我们之前一般称之为全局变量,全局变量在以前会被认为就是window的属性的值; 而ES6中则不是,全局变量和顶层对象的属性的值将脱钩; 具体来说: 1、通过var或者function甚至直接写变量名然后进行赋值创建的对象,其变量名作为key添加到window对象中,而window里该key的值为被赋值的值。 如代码: console.log(window.a); //undefined console.log(window.b); //undefined console.log(window.c); //undefined var a = 1; console.log(window.a); //1 b = 2; console.log(window.b); //2 function c(){} console.log(window.c); //function c(){} 2、而通过let、const,以及之后的class创建的对象,则不会被添加到window里面。 如代码: console.log(window.a); //undefined console.log(window.b); //undefined let a = 1; console.log(window.a); //undefined const b = 2; console.log(window.b); //undefined 顶层对象的获得 简单来说,顶层对象在浏览器里就是window;但是在Node.js里面没有window(Web Worker也没有,他是运行在后台的js脚本); 浏览器和Web Worker里,self指向顶层对象,但是Node.js里没有self; Node里,顶层对象是global,但其他环境不支持(比如chrome里打global会告诉你未定义); 有时候我们需要用同一套代码,但在各个环境拿到顶层对象(啥时候?),所以得找个通用的办法; 但是没有非常完美的。 阮一峰给了两个办法,我直接摘抄了,如下代码: // 方法一 (typeof window !== 'undefined' ? window : (typeof process === 'object' && typeof require === 'function' && typeof global === 'object') ? global : this); // 方法二 var getGlobal = function () { if (typeof self !== 'undefined') { return self; } if (typeof window !== 'undefined') { return window; } if (typeof global !== 'undefined') { return global; } throw new Error('unable to locate global object'); }; 想要了解更多的话,参考阮一峰的博客相关内容。
Babel 参考阮一峰的文章所写。 已细化重点知识,确保可以按步骤复现。并省略某些不常用的内容 解释: 1. 简单来说,就是可以把ES6的代码转换成ES5的代码,这样你就可以在ES5的环境中运行ES6而不必担心兼容性了; 2. ES7的转换也可以靠这个来完成; 3. 其是放置在node_modules文件夹下的插件,就像使用其他通过npm install安装的插件一样使用; 4. 默认只转换语法,不转换API。比如Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise等全局对象,以及一些定义在全局对象上的方法(比如Object.assign)都不会转码。 【使用方法】 1. 在项目的根目录下创建一个文件,文件名是.babelrc,记得放在项目根目录(一般是和package.json还有readme.md同一个目录下); 2. 注意1:上面那个文件,在windows下不能直接创建(会提示说必须输入文件名), 解决方法1:用linux、mac或者用IDE(比如webstorm)来创建; 解决方法2:从我的github中直接下载文件 3. 注意2:那个文件的babelrc就是后缀名,而不是txt格式的(所以会提示没有文件名); 4. 用编译器打开(或者用记事本打开也行,注意编码格式是UTF8),文件内基本格式如下: { "presets": [], "plugins": [] } 5. 这个文件用于设置转码规则和插件; 6. presets是转码规则,值的数组里面,填写规则。 7. plugins是插件,有需要就写,不需要的话这个可以省略。 8. 按需安装转码规则(见下面); 9. 将对应的字符串添加到.babelrc中(千万别忘了,我试了半天总转失败,结果发现我没加) 10. 运行转码命令/内嵌到package.json里在项目运行时转码; 转码规则和转码安装: 1. 首先应该安装对应的转码规则集(他的规则像安装npm插件一样安装); 2. 然后在"presets"这个数组中填写对应的值; 3. 官方提供的规则集如下(转自[阮一峰的博客](http://es6.ruanyifeng.com/#docs/intro)): 注: $表示命令行,实际输入的时候从npm开始输入(应该不会有人不知道吧); #表示注释,不要输这个后面的文字啊 # ES2015转码规则 $ npm install --save-dev babel-preset-es2015 # 添加的字符串为:"es2015"(""表示这是个字符串),下同 # react转码规则 $ npm install --save-dev babel-preset-react # 添加的字符串为:"react" # ES7不同阶段语法提案的转码规则(共有4个阶段),选装一个 $ npm install --save-dev babel-preset-stage-0 $ npm install --save-dev babel-preset-stage-1 $ npm install --save-dev babel-preset-stage-2 $ npm install --save-dev babel-preset-stage-3 # 添加的字符串为:"stage-0"至"stage-3"中的一个(显然后面包含前面) 4. 安装完转码规则之后(根据实际需要安装,不用全部装),在.babelrc文件的"presets"的值中添加对应的字符串作为数组元素,参考上面; 方法一:全局使用babel-cli转码(命令行、单文件、所有文件夹输出结果) 优点: 1.简单直接暴力,全局安装,哪里都能用; 缺点: 1.项目要求有环境依赖,换了环境不能用(比如说换台电脑,但他没装babel-cli就尴尬了); 前置准备: 1. 先配置好,参考【使用方法】 2. 命令行输入以下代码,来全局安装babel-cli工具 npm install --global babel-cli 步骤: 1.控制台输出转换结果(控制台输出): 1. 命令行输入以下代码:(下同,都是控制台输入) babel input.js 2. 然后控制台会输出以下内容(转换结果,下同,结果内容都是以下的内容): "use strict"; var input = []; input.map(function (item) { return item + 1; }); 2.将转换结果输出到指定文件内(单文件转换): 1. 输入: babel input.js -o output.js 2. 输出: 同目录下自动生成output.js,文件内容是上面的转换结果 3. -o表示--out-file,即输出为文件 3.将一个目录下的所有文件(递归执行)全部转码输出到某个文件夹下(同名转换): 1. 先建立一个input文件夹,把之前的input.js复制一份进去; 2. 输入: babel input -d output 3. -d表示--out-dir,即输出到文件夹,前面的input表示输入文件夹名,后面的output表示的是输出的文件夹名; 4. 输出:output目录被创建,里面有input文件夹下的同名input.js,但内容是转换后的 5. input文件下所有文件都会被转换,转换过程是递归的(即子文件夹下的子文件,甚至更深层也会被转换); 非全局使用babel-cli转码(作为项目的依赖转码) 优点: 1.非全局,不要求PC环境全局安装babel-cli; 2.方便版本管理 缺点: 1.需要配置package.json,比较麻烦一些; 前置准备: 1. 先配置好,参考【使用方法】 2. 修改package.json,添加相应的脚本代码; 3. 具体来说,根目录下创建一个package.json,然后文件内容如下: { "devDependencies": { "babel-cli": "^6.0.0" }, "scripts": { "build": "babel input -d output" } } 转换方法: 1. 先建立一个input文件夹,把之前的input.js复制一份进去; 2. 然后在根目录(即package.json以及.babelrc所在目录的控制台输入: npm run build 3. 效果和全局使用的【转换方法3】是一样的; 注1: npm run build里的build,指的是package.json里面,scripts里的build属性的属性名,如果把属性名改为test,那么就是npm run test 注2: npm run build相当于执行了babel input -d output这个指令。只不过这里的babel来源于node_modules文件夹下的babel-cli,而不是之前通过控制台运行的全局的babel-cli 其他转码 如: 提供一个可以直接运行ES6的REPL环境,无需转码直接运行ES6脚本,点击直达 加钩子,每当使用require加载.js、.jsx、.es和.es6后缀名的文件,自动转码点击直达 对某些代码进行转码(按需转码)点击直达 对API进行转码 点击直达 在浏览器环境中实时转码(会影响性能,而以上是直接转完后发给浏览器) 点击直达 还有在线转换(输入ES6代码,输出ES5代码,然后复制拿走使用) 点击直达%3B) 还有关于Google公司的Traceur转码器,或者是babel和其他框架的配合等,请点击右方链接直达阮一峰的博客来查阅 关于插件 1、例如Object.assign这样的方法,在IE下不行。那么就需要用插件 如这个插件:https://babeljs.io/docs/plugins/transform-runtime/ 就可以让IE支持这个功能 或者 另外提一句,Babel默认情况下,是不能转换Set和Map等数据类型的,引自: Babel 默认只转换新的 JavaScript 句法(syntax),而不转换新的 API,比如Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise等全局对象,以及一些定义在全局对象上的方法(比如Object.assign)都不会转码。举例来说,ES6在Array对象上新增了Array.from方法。Babel 就不会转码这个方法。如果想让这个方法运行,必须使用babel-polyfill,为当前环境提供一个垫片。 阮一峰 我自己实践测试来看: 必须引用babel-polyfill才能正常运行Set和Map类型(不然会报错); 引入的方法就是安装这个插件,然后import或者require他就行; 但单独js引入是不行的,需要利用webpack之类的打包(因为一般情况,浏览器是不支持直接跑js文件的require语法);
ajax的手写、封装和自定义设置 1、目标 如果只是会用ajax就行,建议使用jquery等提供ajax功能的库,简单暴力兼容性强还不容易出错。 这里是通过学习ajax来提高自己对ajax、http协议的理解。 手写一次完整的ajax(初级) 1、目标 理解如何手写一次完整的ajax,发起成功的请求,并能成功响应服务器回复的内容。 2、准备工作 一个能跑起来的后端,并预置了简单接口。 我这里提供一个github的简单后台,链接,原DEMO是用jquery写的ajax。具体可以参照该demo的说明。 当写好自己的代码并测试时,将文件放入该demo的public文件夹下,运行该后台,并通过相应的链接来访问。 假如文件是abc.html,那么访问链接是: http://127.0.0.1:3000/abc.html 3、梳理ajax流程 1、创建一个XMLHttpRequest对象的实例(以下过程都是基于这个实例的,每一次ajax是一次实例); 2、设置该实例的ajax请求发向的目标,发起的方式; 3、设置该实例在响应状态发生变化时,执行的回调函数,包括请求成功后的设置,注意,这个函数会被多次执行(每次状态变化都会执行); 4、配置一些ajax的选项(开始初学时可以忽视); 5、发送请求。 6、(自动执行预置好的功能)当发送请求之后,每次实例的readyState属性发生改变时,响应函数都会执行一次,当然,也包括请求成功时。 因此我们需要干的事情就是: 创建一个用于ajax的实例——》各种设置,包括请求失败成功后的——》发送请求,并在请求状态发生变化时,执行之前设置的方法 由于ajax是异步的,而异步的过程中,会有状态变化,因此,有属性readyState表示当前的异步请求处于一种什么样的状态。 请参照MDN的说明来理解。 简单来说,当readyState属性的值为4时,才代表这次异步请求完成(无论是成功还是失败) 以下是一个最简单的ajax处理流程图,以帮助理解: 4、实际写一次ajax 1、创建一个XMLHttpRequest对象的实例。需要注意,每一次ajax都需要new一个实例,不能重复使用; var req = new XMLHttpRequest(); 2、设置该实例的响应函数; req.onreadystatechange = function () { //代码先略过不写,后面再具体细说 } 3、调用实例的open方法,用于设置url和请求方式 //请求方式:类型是字符串,比如"get","put","post"等,大小写无影响 //URL:类型是字符串,请求链接,例如"/getForLearn" //第三个参数值为true表示是异步,值为false表示是同步。不填写的话,默认为true req.open(请求方式, URL, true); 4、发出请求。 //默认情况下,不加参数,支持程度,最低IE7 //可加参数,参照MDN,兼容程度需要至少IE10 req.send(); MDN关于send的说明 只需要以上四步即可,便可完成一次ajax请求。(注:这里省略了一些配置) 如何处理ajax的响应,是通过第二步的函数来设置的。因此我们在这里补充写第二步之前略过没写的函数 //处理函数 req.onreadystatechange = function () { //本函数会被触发多次。 //核心是通过判断this.readyState的值来判断当前的异步请求处于哪一步了。 //当值不等于4时,说明请求尚未完成(无论是成功或者失败) //通过console.log来帮忙理解这个数值的变化 console.log(this.readyState); if (this.readyState !== 4) { return; } //到目前这一步时,说明请求完成了(没完成在之前就return返回了) //然后判断请求状态是否为200,当为200时,说明请求成功 if (this.status === 200) { //this.response 该变量表示服务器返回的内容 console.log(this.response); } else { // 报错,如果是页面没找到的话,这里会是"Not Found" console.error(this.statusText); } } 因此,一个简单的发起ajax请求的代码如下: //细节注释已省略,请参照上面 //1、创建实例 var req = new XMLHttpRequest(); //2、状态变化处理函数,即ajax成功或失败都在这里处理 req.onreadystatechange = function () { if (this.readyState !== 4) { return; } if (this.status === 200) { console.log(this.response); } else { console.error(this.statusText); } } //3、建立连接 req.open(请求方式, URL); //4、发送请求 req.send(); 在上面,我给了一个可以跑起来的后端的github地址,有预设的get响应,建议大家自己通过以上代码去尝试一下。 手动封装ajax(简单版) 1、目标 像jquery那样,写一个类似以下的代码 ajax(options).done(成功时执行的回调函数).fail(失败时执行的回调函数) 由于是初级版,我们不做过多的设置。只需要确保: 1、可以发起ajax请求,可以设置get或post方法, 2、在成功后可以通过自定义的相关回调函数来处理; 3、可以在失败后,通过另一个自定义的相关回调函数来处理; 2、功能实现 1、首先看目标,ajax函数是主函数,他接受一个参数options,这里参数是一个对象,里面有url、type和data三个属性; 2、ajax函数的返回值,可以调用done方法和fail方法,因此他的返回值必然是一个对象,并且有这2个方法; 对于【1】中的情况,我们需要进行以下处理: 1、由于是简单版,所以我们不用过多考虑错误处理的问题; 2、检查options是否是对象; 3、检查属性是否有url和type属性; 4、如果有data属性,根据type来决定如何处理data属性; 对于【2】中的情况,我们需要进行以下处理: 1、done和fail里面是一个回调函数,但该函数不会立刻执行,而是在异步请求完成后才执行; 2、因此需要暂时将设置的这2个回调函数存储起来,只有在符合条件的情况下才执行; 3、因此需要将这2个存储的回调函数放在req.onreadystatechange的函数中; 4、让这2个函数在req.readyState的值为200时执行; 5、需要区分成功和失败; 由于函数可以连写,可以我们必须先处理done和fail的问题,也就是【2】中的问题 1、对done和fail的初步处理 连写的关键在于返回一个对象,并且每次执行时,返回的都是同一个对象; 且该对象需要具有done和fail两个函数属性(不然执行时会报错); 更细节的解释清参照注释 var ajax = function (options) { //创建一个空对象,之后将返回它 var obj = {}; //分别表示成功和失败时,执行函数的变量 var success = function () { }; var fail = function () { }; //设置空对象的done和fail方法,确保能执行起来 //由于可以连写,因此确保done和fail两个方法,在执行后的返回值也需要是obj对象(不然无法连写了) //回调函数作为参数传入,赋值给内置的变量 //注意,此时只是赋值,没有执行 obj.done = function (success2) { success = success2; return obj; } obj.fail = function (fail2) { fail = fail2; return obj; } //... //一些中间省略掉的代码 return obj; } 当有以上代码时,可以至少那一串ajax函数执行完后不会报错了; 下来,我们开始处理options这个参数,首先检查参数是否合法 2、options的属性检查 参数必须是对象,而包含type和url两个属性,且属性类型必须是字符串 //对象类型检查,首先要求参数必须是对象 //然后如果url或者type类型需要是字符串 //如果以上任何一个不通过,则报错 if (!(options instanceof Object) || (typeof options.url !== 'string') || (typeof options.type !== 'string')) { //给个报错的提示信息呗 console.error("error arguments for ajax"); return obj; } options属性我们已经处理了两个了,还有一个可选属性data; 虽然可选,但我们可不能忽视他的存在,下来处理这个data属性 3、data的处理 首先没有data属性肯定就不处理了; 其次,data属性的类型很多,我们应支持尽可能多的种类; 第三,根据get或者post,我们应该将data以不同方式进行处理; 这个处理函数比较长,很多步已经简化处理; 如果是初学者,也可以直接跳过这部分内容,假装传的值一定是正常的(反正初学者肯定用jQuery等成熟库) //只有有data属性的时候才需要进行处理, if (typeof options.data !== 'undefined') { //假如data属性是一个函数,那么跳过,就当没有 if (typeof options.data !== 'function') { // 假如类型是不是get,那么很好处理,因为都被放在请求体之中了 // 直接通过JSON.stringify()方法转换使用 // 记得,需要转为小写(因为可能用户是大写的) if (options.type.toLowerCase() !== 'get') { //默认有JSON.stringify()方法,如果没有则报错 if (typeof JSON.stringify !== 'function') { console.error("can't use JSON.stringify(), so can't Ajax by post when type of data is object"); return obj; } //通过内置方法转为JSON字符串 var data = JSON.stringify(options.data); } else { //此时请求类型必然是get //为了简化,我们这么处理 //当data类型是对象时,以key1=val1&key2=val2这样拼接起来 //当data类型为字符串或数字时,直接添加到url后; //当data类型为其他时,不发起请求并报错 if (typeof options.data === 'string' || typeof options.data === 'number') { var data = options.data; } else if (options.data instanceof Object) { //一个临时数组,用于存放拼接的字符串 var tempArray = []; //注意,由于data可能有某属性也是对象或数组或其他类型; //我们的处理方案是,假如某属性是对象、数组、函数,则直接跳过就当没有 //其他则添加到我们的字符串中 for (var k in options.data) { if (typeof options.data[k] !== 'object' && typeof options.data[k] !== 'function') { tempArray.push(k + "=" + options.data[k]); } } //有长度的话则拼接起来 if (tempArray.length > 0) { var data = tempArray.join("&"); } } } } } 在这一步完成后,我们可能遇见三种情况: 1. 有data属性,那么data属性必然是字符串; 区别要么是json字符串(提供给非get请求),或者kv拼接字符串(提供给get请求的url使用); 2. 要么没data属性,data的值为undefined; 3. 甚至直接报错返回,那么跟之后的内容无关; 3. 于是data情况简单,方便之后处理使用 4、该创建ajax实例了 我们需要创建一个XMLHttpRequest的实例,用于发起ajax请求; 创建这个对象总共需要四步,我们先处理前两步: 第一步创建XMPHttpRequest()实例很简单; 而第二步也不难,因为处理函数只需要执行用户传入的回调函数即可,如代码: var req = new XMLHttpRequest(); req.onreadystatechange = function () { console.log(this); //当属性值不是4的时候,说明ajax没有完成,因此返回不做处理 if (this.readyState !== 4) { return; } // 当ajax的请求完成后,status状态码会发生改变 // 其值来源于Http的头部的Status Code属性 // 可以打开chrome控制台,查看network; // 然后选择一项请求后查看Headers选项卡中,General中的Status Code属性 // 当值为200时,说明成功获取,否则失败 if (this.status === 200) { //success是用户自己写的处理回调函数,我们将返回值作为参数传递 //并执行用户自定义的回调函数 success(this.response); } else { //fail则是用户写的失败处理函数,同样将错误文本作为参数传递,并执行之 fail(this.statusText); } } 5、设置请求链接并传递数据 最后,我们需要设置用户请求的链接,请求类型; 以及将数据(乳沟有的话)作为参数传递给服务器; 注意,数据需要根据用户的请求方式决定如何传递; 唯一需要注意的是,当以post等方式发起ajax请求时,我们需要设置一下请求头的Content-Type属性,告诉服务器我们发送的是JSON字符串 //区分请求方式,决定data数据的传递方法 if (options.type.toLowerCase() === 'get') { //当是get请求时,数据是作为url链接传递给字符串的 if (typeof data !== 'undefined') { //但前提是data,设置url,第三个参数true或者默认值表示是异步 req.open(options.type, options.url + "?" + data, true); //发送请求,并返回 req.send(); } else { //直接设置url即可 req.open(options.type, options.url, true); //发送请求,并返回 req.send(); } } else { //非get方式时,data处理都是一样的,即放在请求体之中 //先设置url req.open(options.type, options.url, true); //然后查看data是否存在 if (typeof data !== 'undefined') { //如果存在,那么显然是JSON格式字符串(因为我们前面已经处理过了) //但是在发送前,我们需要设置一下请求头的Content-Type属性,告诉服务器我们发送的是一个json req.setRequestHeader("Content-Type", "application/json"); //然后再发送 req.send(options.data) } else { //不然直接发送就行 req.send(); } } 最后,由于顺利的设置完了,因此返回obj对象,以确保ajax函数的连写不会报错 //最终返回obj对象 return obj; 3、总结 回顾一下我们做了些什么事情: 1、为了确保可以连写,我们返回了一个符合要求的对象,具有两个同名函数; 2、并且在执行同名函数时,将用户传递的回调函数赋值给我们用于处理请求时所执行的两个函数; 3、验证了ajax发起时传入的参数,确保他是合法的; 4、将data属性正确的加入了发起的请求之中; 5、成功的发起了请求; 目前仍存在的问题: 1、不支持用户自定义是否将返回结果变为对象,目前只能默认保持原样; 2、对报错信息处理的不全; 3、对请求头缺少自定义设置功能; 以下是代码的github链接: https://github.com/qq20004604/a-ajax-project-for-learner 封装了该方法的html文件是: https://github.com/qq20004604/a-ajax-project-for-learner/blob/master/public/XMLHttpRequest.html 对下一步的计划 初级封装和中级高级封装的区别,我认为在于以下几点: 1、更强的错误识别;2、更多的关于头部的设置,以及请求头、响应头的识别、设置和归类;3、简易方法的支持;4、跨域;5、以及其他; 简单来说,就是让你的ajax更像jQuery封装好的ajax。 当然,更全面的另一面就是,更笨重,更复杂。 所以应该根据实际需要而写。
Vue插件 1、概述 简单来说,插件就是指对Vue的功能的增强或补充。 比如说,让你在每个单页面的组件里,都可以调用某个方法,或者共享使用某个变量,或者在某个方法之前执行一段代码等 2、使用方法 总体流程应该是: 【声明插件】——【写插件】——【注册插件】——【使用插件】 写插件和声明插件是同步的,然后注册到Vue对象中(不用担心重复注册),最后在写Vue组件的时候使用写的插件 声明插件 先写一个js文件,这个js文件就是插件文件,里面的基本内容如下: /* 说明: * 插件文件:service.js * 作者:王冬 QQ:20004604 * */ export default { install: function (Vue, options) { // 添加的内容写在这个函数里面 } }; 其中install的第一个参数Vue表示的是Vue的实例,第二个参数表示的是一些设置选项。 Vue实例好理解,就是Vue对象。 而options设置选项就是指,在调用这个插件时,可以传一个对象。 例如这个对象有一个属性float,然后在写插件的一个方法/变量时,我需要输出一个数字,然后写一个if判断语句, 假如options.float为true时,输出浮点数; 假如为false或undefined(即没传参)时,输出为整数。 具体怎么添加,之后再说。 注册插件 如果使用过Vue-router,就很好理解,通过import引入后,然后通过 Vue.use(插件名) 注册插件; 例如,我们通常在main.js里引入各种东西,并且组件的根实例也在这里 //main.js import Vue from 'vue' import App from './App.vue' //关键是这两行 import service from './service.js' Vue.use(service) new Vue({ el: '#app', render: (h) => h(App) }) 如代码中注释所说,关键是通过import导入service文件,然后在创建根组件之前,让Vue对象通过use方法来注册插件service。 通过这样简单的两步,就可以使用插件了。 3、写插件、使用插件 按照官方文档,写插件有四种方法,先给出官方的代码: //以下内容都是添加到上面install的函数里面的 // 1. 添加全局方法或属性 Vue.myGlobalMethod = function () { // 逻辑... } // 2. 添加全局资源 Vue.directive('my-directive', { bind (el, binding, vnode, oldVnode) { // 逻辑... } ... }) // 3. 注入组件 Vue.mixin({ created: function () { // 逻辑... } ... }) // 4. 添加实例方法 Vue.prototype.$myMethod = function (options) { // 逻辑... } 先给出最常用的:【4. 添加实例方法】的写法和使用方法 3.1【添加实例方法或属性】 1、核心思想: 通过prototype来添加方法和属性。 2、写: //让输出的数字翻倍,如果不是数字或者不能隐式转换为数字,则输出null Vue.prototype.doubleNumber = function (val) { if (typeof val === 'number') { return val * 2; } else if (!isNaN(Number(val))) { return Number(val) * 2; } else { return null } } 3、用: 假设有这样一个组件: <template> <div> {{num}} <button @click="double">点击后让左边的数字翻倍</button> </div> </template> <script> export default{ data(){ return { num: 1 } }, methods: { double: function () { //这里的this.doubleNumber()方法就是上面写的组件里的方法 this.num = this.doubleNumber(this.num); } } } </script> 我们便可以通过点击button按钮,让num的值,在每次点击都翻倍了。 4、假如添加的是属性: 例如: Vue.prototype.number = 1; 会发生什么事情呢? 1、不管是【按值传递类型】还是【按引用传递类型】,该变量都不会被不同组件所共享,更准确的说,假如有A、B两个组件。A组件里的number数值改变,B组件里的number数值是不会跟着改变的。因此不要想着引用这样一个变量,然后修改了A中的值,B里也自动跟着改变了; 2、当组件里没有该属性时,调用时,显示的是通过插件获取的值; 当组件里有该属性时,调用时,显示的是组件里该属性的值; 由此而推,函数也是这样的,组件里的同名函数总是会覆盖插件提供的函数。 也就是说,当插件提供一个属性时,组件里没这个属性,就用插件的属性;组件有,就用组件自己的。 3.2【添加全局方法或属性】 1、核心思想: 就是给Vue对象添加一个属性。 初次接触很容易和上面3.1弄混,实际上,3.1是给组件里使用的,而3.2是给Vue对象使用的。 例如,假如添加一个方法test(),那么: 通过3.1添加,是在组件里,通过this.test()来调用 通过3.2添加,是在外面,通过Vue实例,如Vue.test()来调用 2、写: //放在哪里参考上面 Vue.test = function () { alert("123") } 3、用: //注意先导入Vue对象才能使用 Vue.test() 使用时会执行对应的方法,比如这里就是alert弹窗 4、其他: 别问我如果和Vue本身属性同名会发生什么事情,我没试过=.= 3.3【注入组件】 1、核心思想: 就像写Vue组件时,那样写,方法名保持一致,其会在执行组件对应的方法名之前执行。 2、写: 例如: Vue.mixin({ created: function () { console.log("组件开始加载") } }) 然后这里的代码会在每个组件(包括根组件)的created执行之前执行。 可以自行在每个组件的created方法里写一段console.log来查看测试 可以和【实例属性】配合使用,用于调试或者控制某些功能 // 注入组件 Vue.mixin({ created: function () { if (this.NOTICE) console.log("组件开始加载") } }) // 添加注入组件时,是否利用console.log来通知的判断条件 Vue.prototype.NOTICE = false; 【注入给非Vue实例本身就有的方法】: 假如是写给例如methods属性的某个方法,例如以下注入: Vue.mixin({ methods: { test: function () { console.log("mixin test"); } } }) 那么,组件里若本身有test方法,并 不会 先执行插件的test方法,再执行组件的test方法。 而是只执行其中一个,并且优先执行组件本身的同名方法。这点需要注意 3、用: 不需要手动调用,在执行对应的方法时会被自动调用的(并且先调用插件里的,再调用组件本身的) 4、其他: 1、如果同时有多个插件注入一个方法(例如created,那么会先执行先注入的那个方法,再依次执行后注入的,最后执行组件本身的) 2、注意,像methods属性下的方法,并不会在组件注入后每个都执行,而是只执行一个,并且优先执行组件本身的。 3.4【添加全局资源】 1、核心思想: 添加方法和正常添加方法类似,甚至几乎一样。 可以添加【自定义指令】、【过滤器】、【过渡等】,这里以【过滤器】为例 2、写: 例如: //时间格式化过滤器,输入内容是number或者Date对象,输出是YYYY-MM-DD HH-MM-SS Vue.filter('formatTime', function (value) { Date.prototype.Format = function (fmt) { //author: meizz 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), //季度 "S": this.getMilliseconds() //毫秒 }; if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length)); for (var k in o) if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length))); return fmt; } return new Date(value).Format("yyyy-MM-dd hh:mm:ss"); }) 3、用: 和正常使用一样用就行了,so easy。例如: {{num|formatTime}} 4、其他: 可以用这个找各种有意思的功能,作为插件写好,然后需要的地方导入就行,超级方便! 4、示例demo 附一个有简单功能的示例demo,提供参考使用 /* 说明: * 插件demo,供学习使用 * 本页面用于提供各种处理服务 * 作者:王冬 QQ:20004604 * 功能有: * 1、插件created执行时提示; * 2、 * */ export default { install: function (Vue, options) { // 1. 添加全局方法或属性 // 略 // 2. 添加全局资源 // 时间格式化过滤器,输入内容是number或者Date对象,输出是YYYY-MM-DD HH-MM-SS Vue.filter('formatTime', function (value) { Date.prototype.Format = function (fmt) { //author: meizz 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), //季度 "S": this.getMilliseconds() //毫秒 }; if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length)); for (var k in o) if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length))); return fmt; } return new Date(value).Format("yyyy-MM-dd hh:mm:ss"); }) // 2. 添加全局资源 // 添加注入组件时,是否利用console.log来通知的判断条件,也是组件实例属性 Vue.prototype.NOTICE = true; // 3. 注入组件 // 注入组件,插件加载开始前提示 Vue.mixin({ created: function () { if (this.NOTICE) console.log("组件开始加载") }, methods: { test: function () { console.log("mixin test"); } } }) // 4. 添加实例方法 // 返回数字是输入数字的两倍,如果不是数字或者不能隐式转换为数字,则输出null // 组件实例方法 Vue.prototype.doubleNumber = function (val) { if (typeof val === 'number') { return val * 2; } else if (!isNaN(Number(val))) { return Number(val) * 2; } else { return null } } // 4. 添加实例方法 // 服务组,将实例方法整合到$service中,避免命名冲突 Vue.prototype.$service = { //电话号码合法性检查 telNumberCheck: function (tel) { var pattern = /(^(([0\+]\d{2,3}-)?(0\d{2,3})-)(\d{7,8})(-(\d{3,}))?$)|(^0{0,1}1[3|4|5|6|7|8|9][0-9]{9}$)/; return pattern.test(tel) } } } };
Vue的异步组件 1、前置要求 建议使用webpack; Browserify在默认情况下不支持; 2、用法解释 首先上官网说明: https://cn.vuejs.org/v2/guide/components.html#异步组件 虽然说明是没问题的,但是示例中的写法怪怪的,不符合一般新手学习者在实际使用中的习惯。 嗯,换句话说,这段代码告诉你,通过这种方式引入异步组件,然后他漏掉了一些内容,比如说赋值,如何使用之类。 【1】官方示例代码: Vue.component('async-webpack-example', function (resolve) { // 这个特殊的 require 语法告诉 webpack // 自动将编译后的代码分割成不同的块, // 这些块将通过 Ajax 请求自动下载。 require(['./my-async-component'], resolve) }) 【2】官方示例代码的实际使用方法: 你如果是一个新手,看上去就懵逼了(比如之前的我,完全不知道这个例子是想干嘛) 假如你写一个test.vue文件,在<script>标签里,实际使用方法如下: //test.vue的部分 <script> import Vue from 'vue' //关键是以下这部分代码 //需要将引入的异步组件,赋值给变量searchSearch //然后在下方components对象里,将变量正常添加进去,就可以使用异步组件了 //第一个参数是组件名,第二个是异步引入的方法 const searchSearch = Vue.component('searchSearch', function (resolve) { require(['./service-search.vue'], resolve) }) export default{ data(){ return {} }, methods: {}, components: { searchSearch: searchSearch } } </script> 【3】更简单的异步组件的使用方法 上面代码还是太麻烦了,要引入Vue实例先,然后引入组件,然后才能使用。 教练,有木有更简单的?有~ <script> export default{ data(){ return {} }, methods: {}, components: { searchSearch: function (resolve) { //异步组件写法 require(['./service-search.vue'], resolve) } } } </script> 只需要把原有的searchSearch: searchSearch改为一个函数,然后在函数里异步引入就行。
需求: 1. 自适应父Dom的宽高,但设置canvas元素的最小宽高,小于最小宽高则设置父Dom带滚动条。 2. 窗口大小变化时,重新绘制折线图,以适应新的大小,保证折线图一直以最好效果展现; 3. x轴的坐标尺度为时间,单位是月份,数据类型是数组,数组元素是字符串,格式类似"2016/12"这种,为了方便,假设x轴固定有12个刻度,初始刻度为x=0的位置; 4. y轴尺度为数字,要求跟随数据的最大值而自动变化,以保证良好的可见性,并且需要绘制参考线; 5. 简单来说,参考excel表格自动生成的折线图而绘制。 分析需求: 1. canvas元素要自适配父Dom,因此要设法获得父Dom的大小,然后调整自身的大小; 2. canvas元素的宽高的设置,需要写在html元素中,而不能通过css设置(否则可能出问题),因此应该通过js显式的写出来(例如通过canvas.width这样的方法); 3. 折线图,首先确定xy坐标轴绘制在画布上的范围(比canvas画布小); 4. 然后绘制xy坐标轴; 5. 再确定表示数据的折线的范围,注意,数据折线的范围应该比xy坐标轴的范围要小,且左下角和xy坐标轴的原点重合,如此方能有更好的体验(否则若比如数据折线的最右边和坐标轴的最右边重合,是会容易带来误解的); 6. x轴的时间刻度为12个刻度,且初始刻度为x=0的位置,因此每个刻度的宽度 = 数据折线的总宽度/11; 7. 假如x轴的时间刻度为可变数量,也不难,每个刻度的宽度 = 数据折线总宽度(可确认) / (x轴刻度总数量 - 1) 即可; 8. 比较麻烦的y轴的刻度确认。 9. 首先要确认y轴刻度的最大值,遍历所有数据,取其最大值dataMaxY。 10. 然后将dataMaxY向上取值获得用于计算刻度的最大值MaxY,逻辑是这样的: 假如第一个数字是8或者9,,那么取比开头数字大1的数字。例如2开头就是3,6开头就是7,然后后面用0补足位数(以确保和原来的最大数值是同一个量级的) 假如第一个数字是1开头,例如13,那么取比其前两位大的偶数,例如13的话取14,14的话取16,18或19则取20; 11. 根据MaxY来确定y轴的刻度,存储刻度的为数组,数组的元素个数就是刻度的数量。 12. 当MaxY小于10时,固定刻度为2/格,最大10 13. 当MaxY为1开头时,刻度为2/格,最大刻度比MaxY要大; 14. 当MaxY为2~5开头时,刻度为5/格,最大刻度比MaxY大; 15. 当MaxY为6~9开头时,刻度为10/格,最大刻度比MaxY大; 16. 根据刻度数组,以及数据折线图的绘制区域,绘制参考线; 17. 至此y轴刻度和参考线完; 18. 根据刻度数组的最后一个元素(刻度的最大值),以及数据的值,外加数据折线图区域的坐标,可以绘制出折线图; 19. 绘制折线图时可以顺便写下x轴的刻度(x坐标和数据折线图当前数据坐标相同,y坐标固定); 20. 有必要的话,添加输入验证,如果输入错误,则在绘制区域显示错误文字。 21. 添加各种自定义设置,用于设置文字的样式、颜色,宽度大小等; 分拆需求为函数: 1. 获得并设置canvas标签的最小宽高,然后返回canvas中,绘图区域坐标(指x、y坐标轴的绘图坐标); 2. 绘制x、y坐标轴(不包含刻度); 3. 获得y轴最大尺度; 4. 确定y轴刻度数组; 5. 根据y轴刻度数组,绘制参考线和刻度的数字; 6. 绘制数据折线图和x坐标刻度; 7. 绘图函数,需要重新绘制图时,通过本方法来调用以上方法(本接口暴露出来可被外界调用); 8. 输入检查函数,用于提示错误; 9. 刷新函数,用于在窗口大小变化时主动调用绘图函数重新绘图。 10. 【测试用】自动生成数据的函数; 源代码如下: <html> <head> <meta charset="UTF-8"> <title>用Canvas绘制折线图</title> <style> #test { border: 1px solid black; height: 100%; width: 100%; box-sizing: border-box; } </style> </head> <body> <div id="test"></div> <script> var dom = document.getElementById("test"); //数据源,这里是模拟生成的 //数据生成函数 var caseInfo = (function () { var caseInfo = []; //最大值,实际使用中不要用这一条,这个只是用来生成数据的 var max = Math.pow(10, parseInt(Math.random() * 5)); for (let i = 0; i < 12; i++) { caseInfo.push(parseInt(max * Math.random())); } console.log(caseInfo); return caseInfo; })(); // var caseInfo = [0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0]; var dateText = ["2014/2", "2014/3", "2014/4", "2014/5", "2014/6", "2014/7", "2014/8", "2014/9", "2014/10", "2014/11", "2014/12", "2015/1"]; var draw = new drawCanvas(dom, caseInfo, dateText); // 绘图函数的类,传入的参数依次为,canvas标签应该被放置的父Dom,数据,时间 // 1、父dom:支持自适应,最小宽高(此时会设置父dom的overflow为auto) // 2、数据:标准的为12个数据,类型为number,不足12个会自动用0填充满(填充在数组的开始部分); // 3、时间要求格式为:年份/月份,例如2016/12,类型为字符串,非该格式的会被识别为错误并报错(如需修改请自行更改相关判断部分); // 4、y轴坐标的刻度会根据数据的最大值而自动变化; // 5、x轴坐标的刻度自动设为12格 function drawCanvas(Dom, caseInfoArray, dateTextArray) { // 设置 var color = { xyAxisLine: "#000", //x、y坐标轴的颜色 xScale: "#000", //x轴刻度文字的颜色 yScale: "#000", //y轴刻度文字的颜色 referenceLine: "#bbb", //参考线带颜色 dataLine: "#f6793c", //数据线条的颜色 errorMessage: "#000" //错误提示的颜色 }; var font = { yScale: "Microsoft YaHei 12px", //y轴刻度文字 xScale: "Microsoft YaHei 12px" //x轴刻度文字 }; var dataLineWidth = 3; //数据线条的宽度 var error = { errorCaseInfo: "错误的数据", errorCaseTpye: "数据类型不是数字,无法绘图", errorDate: "错误的时间输入" } // 设置完 //获取基础数据 var canvas = document.createElement("canvas"); Dom.appendChild(canvas); var ctx = canvas.getContext("2d"); var caseInfo = caseInfoArray; var dateText = dateTextArray; //获得并设置canvas标签的最小宽高,然后返回canvas中,绘图区域坐标 var setWidthWithHeight = function (Dom, canvas) { //在dojo中,用aspect.after改造 // window.onresize,每次触发事件时重置一次,并且绘制一次 //获得画布区域的宽度和高度,并重置 if (Dom.clientWidth < 700) { canvas.width = 700; Dom.style.overflowX = "auto"; } else { canvas.width = Dom.clientWidth; } if (Dom.clientHeight < 250) { canvas.height = 250; Dom.style.overflowY = "auto"; } else { canvas.height = Dom.clientHeight; } //坐标轴区域 //注意,实际画折线图区域还要比这个略小一点 return { x: 60 - 0.5, //坐标轴在canvas上的left坐标 y: 40 - 0.5, //坐标轴在canvas上的top坐标 maxX: canvas.width - 60.5, //坐标轴在canvas上的right坐标 maxY: canvas.height - 40.5 //坐标轴在canvas上的bottom坐标 }; } // 绘制x、y坐标轴(不包含刻度) var drawAxis = function (ctx, axis) { ctx.beginPath(); ctx.lineWidth = 1; ctx.strokeStyle = color.xyAxisLine; ctx.moveTo(axis.x, axis.maxY); ctx.lineTo(axis.x, axis.y); ctx.lineTo(axis.x - 5, axis.y + 5); ctx.moveTo(axis.x, axis.y); ctx.lineTo(axis.x + 5, axis.y + 5); ctx.stroke(); // 再画X轴 ctx.beginPath(); ctx.lineWidth = 1; ctx.strokeStyle = color.xyAxisLine; ctx.moveTo(axis.x, axis.maxY); ctx.lineTo(axis.maxX, axis.maxY); ctx.lineTo(axis.maxX - 5, axis.maxY + 5); ctx.moveTo(axis.maxX, axis.maxY); ctx.lineTo(axis.maxX - 5, axis.maxY - 5); ctx.stroke(); // 写y轴原点的数字(注意,虽然是坐标原点,但这个是y轴的) ctx.font = font.yScale; ctx.textAlign = "right"; ctx.fillStyle = color.referenceLine; // 设置字体内容,以及在画布上的位置 ctx.fillText("0", axis.x - 5, axis.maxY); } // 获得Y轴的最大尺度 var getMAXrectY = function (caseInfo) { var theMaxCaseInfo = 0; //用于获取最大值 caseInfo.forEach(function (item) { if (item > theMaxCaseInfo) { theMaxCaseInfo = item; } }); //返回计算出的最大数字 return (function (str) { var number = null; //用于计量坐标轴y轴的最大数字 if (str[0] == 1) { if (str[0] + str[1] >= 18) { number = '20'; } else { if (Number(str[1]) % 2) { number = str[0] + String(Number(str[1]) + 1); } else { number = str[0] + String(Number(str[1]) + 2); } } for (let i = 2; i < str.length; i++) { number += '0'; } } else { number = String(Number(str[0]) + 1); for (let i = 1; i < str.length; i++) { number += '0'; } } return number; })(String(theMaxCaseInfo)); } //划线和确定单元格的逻辑在这里,逻辑确定好后是将单元格放在rectYArray这个数组中 var getDrawYLineLaw = function (MAXrectY) { var rectYArray = []; //当最大案件数小于等于10时,以2为一格 if (MAXrectY <= 10) { console.log(MAXrectY); rectYArray.push(2, 4, 6, 8, 10); } else { var str = String(MAXrectY); var zeroNumber = MAXrectY.length - 2; // 用于填充的0的数量,原因是判断时只判断前一位或两位 var fillZero = String(Math.pow(10, zeroNumber)).replace('1', ''); // 然后先判断首位,如果是1,则最大是之前获取到的最大数值,以2/格为单位 // 如果是2~5,则以5/格为单位 // 如果是6~9,则以10/格为单位 if (Number(str[0]) === 1) { for (var i = 0; i < Number(str[0] + str[1]); i = i + 2) { rectYArray.push(i + 2 + fillZero); } } else if (Number(str[0]) >= 2 && Number(str[0]) < 6) { for (var i = 0; i < Number(str[0] + str[1]); i = i + 5) { rectYArray.push(i + 5 + fillZero); } } else if (Number(str[0]) >= 6 && Number(str[0]) < 10) { for (var i = 0; i < Number(str[0] + str[1]); i = i + 10) { rectYArray.push(i + 10 + fillZero); } } } console.log(rectYArray); return rectYArray; } //画y轴参考线和坐标数字 var DrawYLine = function (ctx, axis, YLineLaw) { // 在得到单元格后,开始绘图,绘出y轴上每个单元格的直线 // Y轴参考线的x坐标是从0到axis.maxX - 10 var yMaxPoint = axis.y + 20; //最上面的y轴坐标 var xMaxPoint = axis.maxX - 10; //最右边的x轴坐标 ctx.strokeStyle = color.referenceLine; for (let i = 0; i < YLineLaw.length; i++) { ctx.beginPath(); // 当前绘制线条的y坐标 let yLine = (YLineLaw[i] - YLineLaw[0] ) / YLineLaw[YLineLaw.length - 1] * (axis.maxY - yMaxPoint) + yMaxPoint; ctx.moveTo(axis.x, yLine); ctx.lineTo(xMaxPoint, yLine); ctx.stroke(); //绘完线条写文字 ctx.font = font.yScale; ctx.textAlign = "right"; ctx.fillStyle = color.yScale; // 设置字体内容,以及在画布上的位置 ctx.fillText(YLineLaw[YLineLaw.length - i - 1], axis.x - 5, yLine + 5); } } //绘制数据 var DrawData = function (ctx, axis, caseInfo, YLineMax, dateText) { // 折线绘图区域的x轴从x=0开始绘图,绘制的最右边是axis.maxX-20(参考线是-10) // y轴是从y=0开始绘制,绘制的最顶部是最顶部参考线的位置(axis.y+20) // 参数依次为:绘图对象ctx,坐标轴区域坐标axis,绘图用的数据caseInfo,Y轴最大值YLineMax,x轴横坐标文字dateText var rect = { left: axis.x, //折线绘图区域的left top: axis.y + 20, //折线绘图区域的top height: axis.maxY - axis.y - 20, //折线绘图区域的bottom width: axis.maxX - 20 - axis.x //折线绘图区域的right }; //绘制数据的折线 ctx.beginPath(); ctx.strokeStyle = color.dataLine; ctx.lineWidth = dataLineWidth; var firstPoint = { x: rect.left + 0.5, //之所以+0.5,是因为rect.x来源于axis.x表示划线,因此少了0.5px宽,这里要弥补上 y: rect.top + (1 - caseInfo[0] / YLineMax) * rect.height + 0.5 } // console.log(firstPoint); ctx.moveTo(firstPoint.x, firstPoint.y); for (let i = 0; i < caseInfo.length; i++) { var point = { x: rect.left + i / 11 * rect.width + 0.5, y: rect.top + (1 - caseInfo[i] / YLineMax) * rect.height }; ctx.lineTo(point.x, point.y); //写x轴坐标文字 ctx.font = font.xScale; ctx.textAlign = "center"; ctx.fillStyle = color.xScale; ctx.fillText(dateText[i], point.x, rect.top + rect.height + 15); } ctx.stroke(); } //错误检查 var inputError = function () { //不是数组 if (!(caseInfo instanceof Array)) { return error.errorCaseInfo; } // 数组数目不足12,用0填充靠前的部分 // 大于12,移除前面的部分 if (caseInfo.length < 12) { while (caseInfo.length < 12) { caseInfo.unshift(0); } } else if (caseInfo.length > 12) { while (caseInfo.length > 12) { caseInfo.shift(0); } } //判断数组每个元素的类型是否是number或者能否转换为number var checkElementType = caseInfo.every(function (item) { //如果强制转换后为NaN,那么 if (typeof item !== "number") { return false; } else { return true; } }) if (!checkElementType) { return error.errorCaseTpye; } // 月份应该是字符串,如2016/2 // 如果被/分割拆分后数组长度不是2,或者拆分后元素0的长度不是4,或者拆分后元素1的长度不是1或2 // 或者parseInt转换后为NaN var checkDateText = dateText.every(function (item) { var date = item.split("/"); if (date.length !== 2 || date[0].length !== 4 || date[1].length < 1 || date[1].length > 2 || isNaN(parseInt(date[0])) || isNaN(parseInt(date[1]))) { return false; } else { return true; } }) if (!checkDateText) { return error.errorDate } return false; } //绘图函数,绘制时调用本函数 this.toDraw = function () { // 设置canvas的Dom的宽高 var axis = setWidthWithHeight(Dom, canvas); // 绘制x、y坐标轴(不包含刻度) drawAxis(ctx, axis); //如果检测返回false // 如果没问题,则返回false,否则值可以隐式转换为true var errorMessage = inputError(); if (errorMessage) { ctx.font = "Bold 20px Arial"; ctx.textAlign = "center"; ctx.fillStyle = color.errorMessage; ctx.fillText(errorMessage, (axis.x + axis.maxX) / 2, (axis.y + axis.maxY) / 2); return; } // 获得Y轴的最大尺度 var MAXrectY = getMAXrectY(caseInfo); // 获得y轴划参考线规则 var YLineLaw = getDrawYLineLaw(MAXrectY); // 绘制Y轴参考线 DrawYLine(ctx, axis, YLineLaw); // 绘制数据 DrawData(ctx, axis, caseInfo, YLineLaw[YLineLaw.length - 1], dateText); }; //启动本实例时绘图一次 this.toDraw(); var self = this; //浏览器窗口大小变化时,绘图一次 //潜在缺点:会覆盖其他的这个方法,建议用jquery的$(window).resize来替代 window.onresize = function () { self.toDraw(); }; } </script> </body> </html>
ES5的Object对象新增API Object.create(proto[, propertiesObject]) 说明: 1. 简单来说,这个用于创建一个新对象; 2. 这个对象首先按引用继承了第一个参数的值, 3. 然后将第二个参数所描述的值添加进去(如果相同则覆盖) 第一个参数: 1. 第一个参数如果是个对象,那么这个对象的值改变时,新对象的同样属性的值也会随之改变; 2. 第一个参数可以是null,返回是Object 3. 可以是一个对象,返回还是Object 4. 也可以是数组(例如var n = Object.create([1], {a: {value: 2}}); 5. 那么返回的是一个Array(通过instanceof得证) 6. 这个数组可以通过n[0]来访问到数组的第一个元素1 7. 也可以通过n.a访问到通过create方法添加进来的属性a的值2 第二个参数: 1. 第二个参数并非常规对象的写法,而是以key和对象组合的形式作为一个k-v组合 2. 他的组织形式是这样的: key: { value:"", enumerable: true, //能否被枚举 writable: true, //能否被修改 configurable: true //属性的特性能否被修改/删除 } 3. value是必须有的,其他三个可以不加,默认取值是false 4. enumerable表示能否被枚举,简单来说,就是for in遍历属性时,是否会遍历到这个属性。true则会,false不会 5. writable表示值赋予之后,能否被修改。true为能,false为不能 6. configurable决定属性的特性能否被修改,属性能否被删除(true能false不能),但不影响对value修改。 7. 假如configurable为false,writable为true,那么value可以被修改,writable可以被修改 8. 但假如在上一步的情况下,writable修改为false,那么就无法改回来了(因为configurable的原因) 9. 修改这三个属性,需要用Object.defineProperty方法来进行,而不能直接通过例如obj.prop.writable这样 关于set和get: 1. 虽然在上面没有写setter和getter,但Object.create创建的属性,的确也是可以用setter和getter的; 2. 虽然没有一一验证,但是可以推断出,类似的方法,应该都可以使用setter和getter,以及上面四个属性的(虽然他们之间会有冲突); 3. setter和getter的介绍请参照Object.defineProperties部分; 4. setter和getter在使用时,会和value以及writable有冲突,具体同样参照下面的说明; 5. setter和getter,就是指set方法和get方法(To 那些诧异setter和getter与set和get之间关系的人); 关于writable: 1. 有些类似const; 2. 假如值是字符串(或其他),那么无法被修改为其他字符串/数值/对象的; 3. 假如值是对象/数组等按引用传递类型,虽然依然不能被修改其指向的对象/数组,但是可以直接修改其子属性的值/添加删除数组; 4. 例如test.props的值是{} 空对象,那么test.props = "abc";是失败的。 5. 但是test.props.a = 'abc'; 是被允许的。 6. 数组类似,可以通过.push像数组新增,或者其他; 7. 也就是说,对于按引用传递类型,不能修改其指向的对象,但是修改其指向对象的值。 Object.defineProperties(obj, props) 说明: 1. 对obj对象添加新属性、修改原属性; 2. 本方法可以同时新增/修改多个属性; 3. 该方法会返回一个对象,这个对象就是被修改对象(即返回值===obj); 参数1: 1. 第一个参数是被修改的对象,略; 参数2: 1. 先解释写一个属性的方法,重点的set和get 2. 第二个参数是一个对象,属性以key-val形式存在于对象中; 示例: { 属性名: { //属性名,例如要设置test.a,那么这里的属性名就是a value: "props3", //值,和get、set不能同时存在。这里四个属性可以参考 writable: true, //是否可修改 configurable: true, //属性是否可修改 enumerable: true, //是否可被for in set: function(val){}, //set方法,不能与value属性同时出现,具体以下细解 get: function(){ return xxx; } //get方法,同上 } } 3. value、writable、configurable、enumerable之前已经介绍过了,略; 4. set和get总是成对出现的,当有set和get时,不能有value和writable属性存在,相反也是,不然会报错; 5. set方法是一个函数,有一个参数val,这个val的值是你写例如test.a = 'abc'; 时的这个字符串abc; 6. 你需要将这个abc赋给对象的某个属性, 7. get方法也是一个函数,没有参数,但需要有返回值,返回值就相当于test.a的值; 8. 还记不记得6中的那个被赋值的属性,这个时候,你将这个值作为return的返回值; 9. 通过5~8这四步,你就相当于实现了value的效果。 10. 潜在缺陷:被set和get方法作为中介的那个值,可能被枚举到或者可以直接从test中查看到(比如通过for in); 11. 解决方法:可以用一个不可被枚举的属性(值是对象)作为存储空间,然后把中间值放在这个存储空间中来存储这个中介值。(具体可以见DEMO) 同时修改多个属性: 1. 在参数二中,并列设置即可; 示例: { 属性1:[ xxxx }, 属性2:{ xxxx }, 属性3:[ xxxx } } 中间具体写法已省略,同前面 兼容性写法: 好像没有很好能兼容的,见过的就算有,代码也很长,所以IE9以下的基本不要考虑了吧 Object.defineProperty(obj, prop, descriptor) 说明: 1. 类似Object.defineProperties,不过本方法一次只能新增/修改一个属性; 2. 第二个参数写Object.defineProperties中的key,类型是字符串; 3. 第三个参数是Object.defineProperties中的val,类型是对象; 4. 第三个参数的写法可以参照上面Object.defineProperties的说明,没有什么不同; 5. 还不懂的话就看DEMO吧。 关于继承: 1. MDN网站说某些选项可能不是自身属性(继承来的),因此要考虑到这种情况; 2. 为了保留这些东西,因此可能要在修改之前冻结Object.prototype(避免因修改而导致源属性也被修改); 3. 也可以将__proto__指向null; 4. 我不是很懂他的意思,我觉得主要是我没明确他指的这件事发生的场景是什么 5. 无论如何,给个链接吧 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty Object.keys(obj) 说明: 1. 效果很简单直白,类似for in,遍历对象,然后把key放在数组里,返回这个数组; 2. 不能被枚举的属性(例如enumerable值为false的),不会被放入; 3. 不能被枚举的属性,假如其子属性可以被枚举到,那么也不会被添加进去; 4. 但是若直接以不能被枚举的属性作为Object.keys的参数,那么子属性假如可以被枚举,则会被添加进去; DEMO链接请参照:https://github.com/qq20004604/some_demo/tree/master/ES5%E4%B8%AD%EF%BC%8CObject%E6%96%B0%E5%A2%9EAPI已按API名字起名 代码较长,故不在本文内附,请自行从以上链接下载
代码见我的github:https://github.com/qq20004604/some_demo/tree/master/3D%E6%88%BF%E9%97%B4 DEMO地址见:(对手机不友好,不建议用手机打开,流量耗费约500kb)http://jianwangsan.cn/3Droom/3Droom.html 3D房间 **前注** 本文内容参考学习【张鑫旭】的文章所写。 DEMO链接域名为zhagnxinyu的,则引自其个人网站。 如果对某些概念不能理解,可以查看以下链接来看示意图。 参考链接: http://www.zhangxinxu.com/wordpress/2012/09/css3-3d-transform-perspective-animate-transition/ **基础** DOM结构如下: 舞台 | |———— 容器1 | | | |———— 图片1 | | | |———— 图片2(以及其他) | | |———— 容器2 | | …… |—— 图片1 | |—— 图片2(以及其他) 3D在Z轴上的结构如下: 视角(用户在屏幕外距离屏幕的距离)——————屏幕(具体而言是舞台元素)——————动画元素(相对舞台元素的深度) perspective: perspective:??px; //问号表示number数值 视角-屏幕的距离; 一般是正值; 当该属性设置给舞台元素时,所有动画元素初始位置距离用户的距离则为该值。 该属性也可以设置给具体一个动画元素,则该动画元素距离用户的距离为该值。 translateZ: transform: translateZ(3000px); //使用时,所有基于transform属性下的属性,应该放在同一条transform属性上(即使一个是Z轴一个是X轴),否则后面使用的会覆盖前面的。 屏幕(舞台元素)和动画元素相对运动的距离; 值为正时,则在Z轴上往观察者(用户)靠近,值为负时,则远离。 当该值和perspective的值相等时,说明该元素和用户视角所在位置重合(刚好看不到该元素); 具体的话,请看DEMO图片(我自己画的): 当视角固定时,某个动画元素靠近用户时,只能看到部分区域的示意图.png **舞台元素** 简单来说,就是所有动画元素的根元素。 所有动画元素都在这个元素上显示,通过设置舞台元素的大小,可以控制显示动画元素的范围。 而通过设置overflow:hidden这样的属性,让动画元素不能超出舞台元素显示。 属性 perspective:??px; //透视属性,问号表示数字 本属性是指,你从距离多少px的位置来查看该元素。 具体而言,指视角(人的眼睛),距离屏幕的距离,所看到的3D模型投影在舞台元素上的2D画面。 简单来说,你透过窗户往外看,窗口大小是固定的,当你距离窗户越近,能看到的外面景色就越多,当你距离窗户越远,能看到外面的景色就越小。(依然是参考上面那个图片) overflow:hidden; //超出部分不显示:即显示投影到视角范围的部分(demo图片的紫色区域) 设置这个属性,可以限制图片只在指定区域内显示。 position:absolute; //设置为非static(默认)属性值,方便子元素定位和调整位置(否则可能不好确定动画元素所在位置)。 用于方便容器定位其相对位置。 perspective-origin:50% 50%; //默认是居中 用于定位观察焦点在舞台元素上的位置,默认是居中。 **容器** 将几个位置相关的动画元素放在同一个容器里,然后只需要设置他们相对于容器的位置即可。(例如创建一个长方体) 当需要调整动画元素的位置、旋转方向等时,只需要调整他们的容器的位置即可,便可以改变容器内所有元素的位置。 属性 transform-style: preserve-3d; 用于应用于父元素,设置其子元素是否继承3d,默认是flat(不继承),在使用3D时,应该使用preserve-3d这个值。 另外,【张鑫旭】的文章中说应该应用给舞台元素,但我在实践中发现,如果取消容器的该属性,单纯给舞台元素的话,则不能正常显示。 transform: translateZ(0px); 用于调整容器的位置。除了Z之外,还可以修改X和Y。或者干脆使用translate3d(x, y, z)这样的形式。 以下指容器移动相对于舞台元素 X:+右-左; Y:+下-上;(这个请参考html的XY轴坐标体系理解) Z:+近-远; transform:rotateX(45deg); //旋转,沿X轴旋转 transform-origin: x y z; //旋转的坐标轴 这2个属性用于对容器进行旋转。 简单来说,如果我们要旋转一个立方体, 首先,肯定要有一根轴线,即rotateX/Y/Z(沿X轴Y轴Z轴旋转); 其次,我们需要确定这跟轴线在三维空间中的位置(注意,这里的三维空间,指的是相对于当前元素:如容器,的三维空间,而不是整个HTML的三维空间); 因此,我们需要通过transform-origin给出的原点,来确定旋转时,坐标的基准点(即计算旋转时,x、y、z轴相交会的那个点);有了点之后,再加上旋转轴就可以确定旋转情况了。 其中,transform-origin的x和y可以使用px也可以使用百分比(其左上角的点坐标为x:0, y:0),默认是50% 50%(居中); 而默认z的值为0,可以使用px,但不能使用百分比。 至于rotateX(45deg),表示沿X轴旋转45度,360deg是旋转一圈(和初始一样),正值是顺时针,负值是逆时针; 另外请注意,旋转的顺时针和逆时针跟轴的方向有关(x向右,y向下,z靠近用户) **动画元素** 多个动画元素放在同一个容器里,组成一个整体(比如说长方体)。 动画元素只需要考虑相对于容器的位置即可。 transform: rotateX(-90deg); transform-origin: 0 0; 同容器元素 backface-visibility: hidden; //背面是否可见,默认是可见 当元素是半透明时,则默认可以看到元素的背面情况。但若属性设置为hidden,则背面元素将不可见。 position设置之后,top、left、right、bottom相关 需要注意,旋转动画元素时,例如长100,宽100,深度是500时,我们需要设置上下左右四个方向的dom的基准位置。例如,放置于下方的元素,其除了设置transform-origin:0 100%;之外,应设置为bottom:0; left:0这样(只有这样才能构成一个长方体,不明白的话仔细想想,或者自己动手试试) 3D坐标计算原理 计划结构如下: 舞台元素 ————】动画容器之根元素 ————————】动画容器一 ————————————】动画元素1、2、3…… ————————】动画容器二 ————————】动画容器三 3D移动原理 由于所有的 动画元素 相对于 动画容器 的坐标是固定的; 动画容器元素 相对于 动画容器根元素 是固定的; 因此,只需要控制 动画容器跟元素 的旋转坐标圆心和旋转角度,即可形成整个场景的变换。 可能存在的潜在缺陷:假如场景里的结点很多,也许会有性能方面的问题?不确定。 有没有替代的方案?不确定 计算函数 基础属性: var xyz = { x: 250, //xyz的坐标 y: 250, z: 0, rotate: 0 //面向角度 } 角度弧度换算: //通过角度来得到弧度 function getRadianFromAngle(angle) { return angle * Math.PI / 180; } XZ平面上前进的计算函数: //前进的话,根据角度计算坐标变化 function goFrontAndGetNewValue() { var radian = getRadianFromAngle(xyz.rotate); var x = Math.sin(radian) * 50; var z = Math.cos(radian) * 50; x = Number(x.toFixed(2)); z = Number(z.toFixed(2)); //首先让rotate的值是0~359之间 if (xyz.rotate >= 360) { xyz.rotate -= 360; } else if (xyz.rotate < 0) { xyz.rotate += 360; } if (xyz.rotate < 90) { //右上方角度 xyz.x += x; xyz.z -= z; console.log("右上"); } else if (xyz.rotate < 180) { //右下方角度 xyz.x += x; xyz.z -= z; console.log("右下"); } else if (xyz.rotate < 270) { //左下方角度 xyz.x += x; xyz.z -= z; console.log("左下"); } else if (xyz.rotate < 360) { //左上方角度 xyz.x += x; xyz.z -= z; console.log("左上"); } console.log(xyz); } 前进行为完成的函数: //先计算坐标变化,然后计算当前的坐标中心(方便之后旋转) //前进 function GoAhead() { var transformValue = parent.style.transform; goFrontAndGetNewValue(); //先设置位移 var newString = 'translate3d(' + (-xyz.x) + 'px,' + 0 + 'px,' + (-xyz.z) + 'px)'; parent.style.transform = transformValue.replace(/translate3d\([^)]+\)/, newString); //然后设置当前旋转中心 var originValue = String(250 + xyz.x) + 'px ' + String(250 + xyz.y) + 'px ' + String(xyz.z) + 'px'; console.log(originValue); parent.style['transform-origin'] = originValue; } 后退同理,其中计算坐标变化的函数,把+x变为-x(-x变为+x),z轴同理,变换正负即可。 向上看: //原理是以X轴为坐标旋转整个容器,向上看即为把容器向下旋转。但需要注意,是以当前旋转情况下的X轴来旋转 下来复习3d旋转的css属性: rotate3d(x, y, z, 角度) //以从0,0,0到参数的点为轴,进行3d旋转 transform:rotate3d(1, 0, 0, 45deg); 相当于以x轴为轴进行旋转 transform:rotate3d(0, 0, 1, 45deg); 以z轴进行旋转(顺时针) 具体来说,从参数给的坐标点,看向原点,然后以这个状态进行顺时针旋转。 注意: 《1》y轴的正数为靠近观测点,负数为远离。 《2》值用1或者100没区别(因为只是表达方向); 《3》这里的x、y、z的值,与其他坐标无关(即例如不受translate3d这样属性的影响) 因此,我们需要得知从哪里(观察点,即上面的x,y,z坐标)来旋转这个3d模型。 计算方法: 讲道理说,这个不是很好理解,需要有一定3d空间想象的能力。这里使用rotateY来确定左右旋转的情况,然后使用rotate3d来确定左右旋转后,上下旋转的情况。 1、由于我们已知当前点的x、y、z坐标,因此,在计算中假设其为0,在最终算出来的结果上再加上当前点的x、y、z坐标值即可; 2、我们需要有2个圆,第一个圆用于确定左右旋转,第二个圆用于确定上下旋转。 3、第一个圆使用rotateY属性,他的初始三点坐标是:圆心O(0,0,0),圆顶点A(0,1,0),圆第三点B(1,0,0)。这个圆面对的方向(OB射线的左边为面对方向,例如,初始情况下,面向指从(0,0,0)-(1,0,0)这个线段往(0,0,-1)-(1,0,-1)这个线段看去。注意:z轴远离我们是负值),即为我们观察的方向。其中:O和A为固定值,B点以O为圆心,长度1为半径,在XZ轴平面移动,即可改变代表面向的方向。例如,当B旋转-90deg时,我们正好看向初始情况下,我们左手边(rotateY(-90deg)),此时,B坐标为(0,0,-1); 4、第二个圆根据第一个圆而变化。具体而言,第二个圆的圆心O为(0,0,0),其中一个点为第一个圆的点B,另一个点初始是(0,1,0),但无需去注意他,因为我们只需要知道第二个圆的倾斜角度即可,具体而言,是rotate3d的第四个参数。其中,正值往上看,负值往下看。 5、因此,我们最重要的是,计算出第一个圆的点B的坐标,而这个坐标,可以根据左右旋转时的角度得知。 6、计算方法: x坐标:1*sin(旋转的弧度); y坐标:固定为0; z坐标:1*cos(旋转的弧度);
本篇资料来于官方文档: http://cn.vuejs.org/guide/components.html 本文是在官方文档的基础上,更加细致的说明,代码更多更全。 简单来说,更适合新手阅读 (二十五)组件的定义 ①组件的作用: 【1】扩展HTML元素,封装可重用的代码; 【2】组件是自定义元素,Vuejs的编译器可以为其添加特殊的功能; 【3】某些情况下,组件可以是原生HTML元素的形式,以is的方式扩展。 ②写一个标准的组件: 分为以下几步: 【1】挂载组件的地方,需要是Vue实例所渲染的html元素,具体来说,比如上面的<div id=”app”></div>这样的html元素及他的子节点; 【2】定义一个组件,用 var 变量名 = Vue.extend({template:”这里是html的模板内容”}) 这样的形式创建,例如: //定义一个组件var btn = Vue.extend({ template: "<button>这是一个按钮</button>"}) 【3】将定义的组件注册到Vue实例上,这会让指定标签,被组件的内容所替代。 如代码: //注册他到Vue实例上Vue.component("add-button", btn); 具体而言,每一个以下这样的标签(在Vue的根实例范围内的) <add-button></add-button> 会被 <button>这是一个按钮</button> 所替代。 【4】以上方法是全局注册(每个Vue实例的add-button标签都会被我们定义的所替代); 解决办法是局部注册。 如代码:(这是是设置了template属性,也可以在没有这个属性的时候,在<div id=”app”></div>标签内放置<add-button></add-button>标签 <div id="app"> </div> <script> //定义一个组件 var btn = Vue.extend({ template: "<button>这是一个按钮</button>" }) Vue.component("add-button", btn); //创建根实例,也就是说让Vue对这个根生效 var vm = new Vue({ el: '#app', template: "<add-button></add-button>" });</script> ③局部注册组件: 简单来说,只对这一个Vue实例生效,具体做法是,在注册那一步,跳过; 然后在声明Vue实例的时候,将添加到components这个属性中(他是一个对象,以KV形式放置)(注意,这个单词多一个s) 如代码: <div id="app"> </div> <script> //定义一个组件 var btn = Vue.extend({ template: "<button>这是一个按钮</button>" }) //创建根实例,也就是说让Vue对这个根生效 var vm = new Vue({ el: '#app', template: "<add-button></add-button>", components: { "add-button": btn } });</script> 注: 根据官方教程,这种方法(指局部注册),也适用于其他资源,比如指令、过滤器和过渡。 ④步骤简化: 【1】定义组件和注册组件结合起来一步完成: //定义一个组件Vue.component("add-button", { template: "<button>这是一个按钮</button>"}); 【2】局部注册时,定义和注册一步完成: //创建根实例,也就是说让Vue对这个根生效var vm = new Vue({ el: '#app', template: "<add-button></add-button>", components: { "add-button": { template: "<button>这是一个按钮</button>" } } }); ⑤data属性 直接给组件添加data属性是不可以的(无效); 原因在于,假如这么干,那么组件的data属性有可能是一个对象,而这个对象也有可能是外部传入的(例如先声明一个对象,然后这个对象作为data的值),可能导致这个组件的所有副本,都共享一个对象(那个外部传入的),这显然是不对的。 因此,data属性应该是一个函数,然后有一个返回值,这个返回值作为data属性的值。 且这个返回值应该是一个全新的对象(即深度复制的,避免多个组件共享一个对象); 如代码: var vm = new Vue({ el: '#app', template: "<add-button></add-button>", components: { "add-button": { template: "<button>这是一个按钮{{btn}}</button>", data: function () { return {btn: "123"}; } } } }); 另外,假如这样的话,btn的值是一样的(因为他们实际上还是共享了一个对象) <div id="app"> </div> <div id="app2"> </div> <script> var obj = {btn: "123"}; var vm = new Vue({ el: '#app', template: "<add-button></add-button>", components: { "add-button": { template: "<button>这是一个按钮{{btn}}</button>", data: function () { return obj; } } } }); obj.btn = "456"; var vm2 = new Vue({ el: '#app2', template: "<add-button></add-button>", components: { "add-button": { template: "<button>这是一个按钮{{btn}}</button>", data: function () { return obj; } } } });</script> 注1: el属性用在Vue.extend()中时,也须是一个函数。 注2: 给子组件添加methods无需使用函数返回值,直接如正常那样添加即可。 示例代码: <div id="app"> 子组件: <test></test> </div> <script> var vm = new Vue({ el: '#app', data: { val: 1 }, components: { test: { props: ['test'], template: "<input @keyup='findParent' v-model='test'/>", methods: { findParent: function () { console.log("happened"); } } } } });</script> ⑥is特性: 【1】按照官方教程,一些HTML元素对什么元素可以放在它之中是有限制的; 简单来说,如果我要在table标签内复用某个组件,这个组件展开后是tr标签,但是展开前不是,那么就无法正常运行(被放置在table标签内); 如代码(错误写法,会渲染错误): <div id="app"> <table> <tr> <td>索引</td> <td>ID</td> <td>说明</td> </tr> <thetr v-for="i in items" v-bind:id="i" :index="$index"></thetr> </table> </div> <script> var vm = new Vue({ el: '#app', data: { items: [1, 2, 3, 4] }, methods: { toknowchildren: function () { //切换组件显示 console.log(this.$children); } }, components: { thetr: { //第一个子组件 template: "<tr>" + "<td>{{index}}</td>" + "<td>{{id}}</td>" + "<td>这里是子组件</td>" + "</tr>", props: ['id', 'index'] } } });</script> 渲染结果如下: <div id="app"> <tr><td>0</td><td>1</td><td>这里是子组件</td></tr> <tr><td>1</td><td>2</td><td>这里是子组件</td></tr> <tr><td>2</td><td>3</td><td>这里是子组件</td></tr> <tr><td>3</td><td>4</td><td>这里是子组件</td></tr> <table> <tbody> <tr> <td>索引</td> <td>ID</td> <td>说明</td> </tr> </tbody> </table> </div> 可以明显发现,内容没有被放在table之中。 正确写法如下: <div id="app"> <button @click="toknowchildren">点击让子组件显示</button> <table> <tr> <td>索引</td> <td>ID</td> <td>说明</td> </tr> <tr is="thetr" v-for="i in items" v-bind:id="i" :index="$index"></tr> </table> </div> 【2】更多冲突参照URL http://cn.vuejs.org/guide/components.html#u6A21_u677F_u89E3_u6790 (二十六)props数据传递 ①组件实例的作用域: 是孤立的,简单的来说,组件和组件之间,即使有同名属性,值也不共享。 <div id="app"> <add></add> <del></del> </div> <script> var vm = new Vue({ el: '#app', components: { "add": { template: "<button>btn:{{btn}}</button>", data: function () { return {btn: "123"}; } }, del: { template: "<button>btn:{{btn}}</button>", data: function () { return {btn: "456"}; } } } });</script> 渲染结果是: 2个按钮,第一个的值是123,第二个的值是456(虽然他们都是btn) ②使用props绑定静态数据: 【1】这种方法用于传递字符串,且值是写在父组件自定义元素上的。 【2】下面示例中的写法,不能传递父组件data属性中的值 【3】会覆盖模板的data属性中,同名的值。 示例代码: <div id="app"> <add btn="h"></add> </div> <script> var vm = new Vue({ el: '#app', data: { h: "hello" }, components: { "add": { props: ['btn'], template: "<button>btn:{{btn}}</button>", data: function () { return {btn: "123"}; } } } });</script> 这种写法下,btn的值是h,而不是123,或者是hello。 【4】驼峰写法 假如插值是驼峰式的, 而在html标签中,由于html的特性是不区分大小写(比如LI和li是一样的),因此,html标签中要传递的值要写成短横线式的(如btn-test),以区分大小写。 而在props的数组中,应该和插值保持一致,写成驼峰式的(如btnTest)。 例如: props: ['btnTest'],template: "<button>btn:{{btnTest}}</button>", 正确的写法: <add btn-test="h"></add> 假如插值写短横线式,或者是html标签写成驼峰式,都不能正常生效。(除非插值不写成驼峰式——跳过大小写的限制,才可以) ③利用props绑定动态数据: 简单来说,就是让子组件的某个插值,和父组件的数据保持一致。 标准写法是(利用v-bind): <add v-bind:子组件的值="父组件的属性"></add> 如代码: <div id="app"> <add v-bind:btn="h"></add> </div> <script> var vm = new Vue({ el: '#app', data: { h: "hello" }, components: { "add": { props: ['btn'], template: "<button>btn:{{btn}}</button>", data: function () { return {'btn': "123"}; //子组件同名的值被覆盖了 } } } });</script> 说明: 【1】btn使用的父组件data中 h的值; 【2】子组件的data的函数中返回值被覆盖了。 【3】也就是说,使用v-bind的是使用父组件的值(根据属性名),没有使用v-bind的是将标签里的数值当做字符串来使用。 【4】依然需要使用props,否则他会取用自己data里的btn的值 ④字面量和动态语法: 【1】简单来说,不加v-bind的,传递的是字面量,即当做字符串(例如1也是字符串,而不是number类型); 【2】加上v-bind的,传递的是JS表达式(因此才能传递父组件的值); 【3】加上v-bind后,如果能找到父组件的值,那么使用父组件的值;如果没有对应的,则将其看做一个js表达式(例如1+2看做3,{a:1}看做是一个对象); 如代码: <div id="app"> <add v-bind:btn="1+2"></add> </div> <script> var vm = new Vue({ el: '#app', data: { h: "hello" }, components: { "add": { props: ['btn'], template: "<button>btn:{{btn}}</button>" } } });</script> 这里的btn的值是3(而不是没有加v-bind时,作为字符串的1+2) ⑤props的绑定类型: 【1】简单来说,分为两种类型,即单向绑定(父组件能影响子组件,但相反不行)和双向绑定(子组件也能影响父组件); 【2】单向绑定示例:(默认,或使用.once) <div id="app"> 父组件: <input v-model="val"><br/> 子组件: <test v-bind:test-Val="val"></test> </div> <script> var vm = new Vue({ el: '#app', data: { val: 1 }, components: { "test": { props: ['testVal'], template: "<input v-model='testVal'/>" } } });</script> 说明: 当父组件的值被更改后,子组件的值也随之更改; 当子组件的值被更改后,父组件的值不会变化,而假如再次修改父组件的值,子组件会再次同步。 另外需要注意的是,子组件如果要同步绑定,那么子组件的input需要是v-model,而不能是value属性(那样只能单项绑定,且修改子组件的值后会失去绑定) 【3】双向绑定: 需要使用“.sync”作为修饰词 如示例: <div id="app"> 父组件: <input v-model="val"><br/> 子组件: <test :test.sync="val"></test> </div> <script> var vm = new Vue({ el: '#app', data: { val: 1 }, components: { "test": { props: ['test'], template: "<input v-model='test'/>" } } });</script> 效果是无论你改哪一个的值,另外一个都会随之变动。 【4】props验证: 简单来说,当组件获取数据时,进行验证,只有符合条件的时候,才会使用之。 写法是将props变为一个对象,被验证是值是对象的key,验证条件是和key对应的value。 例如: props: { test: { twoWay: true } }, 验证test这个变量是不是双向绑定,如果不是,则报错。(注意,这个不能用于验证单向绑定)。 示例代码如下: <div id="app"> 父组件: <input v-model="val"><br/> 子组件: <test :test="val"></test> </div> <script> var vm = new Vue({ el: '#app', data: { val: 1 }, components:{ test:{ props: { test: { twoWay: true } }, template: "<input v-model='test'/>" } } });</script> 更多验证查看官方教程: http://cn.vuejs.org/guide/components.html#Prop__u9A8C_u8BC1 (二十七)父子组件通信 ①访问子组件、父组件、根组件; this.$parent 访问父组件 this.$children 访问子组件(是一个数组) this.$root 根实例的后代访问根实例 示例代码: <div id="app"> 父组件: <input v-model="val"><br/> 子组件: <test :test="val"></test> </div> <script> var vm = new Vue({ el: '#app', data: { val: 1 }, components: { test: { props: ['test'], template: "<input @keyup='findParent' v-model='test'/>", methods: { findParent: function () { console.log(this.$parent); //访问根组件 console.log(this.$parent.val); //访问根组件的val属性 console.log(this.$parent.$children.indexOf(this)); //查看当前能否在其父组件的子组件中找到索引 console.log(this.$parent === this.$root); //查看父组件和根组件是不是全等的(因为他的父组件就是根组件) } } } } });</script> 当在子组件的输入框按键弹起时,显示内容依次为: 父组件、父组件的输入框的值(默认情况是1)、0(表示是父组件的children属性中的第一个元素)、true(由于父组件就是根组件,所以是全等的); 通过这样的方法,可以在组件树中进行互动。 ②自定义事件: 首先,事件需要放置在events属性之中,而不是放置在methods属性中(新手很容易犯的错误),只能触发events属性中的事件,而methods属性中的事件是无法触发的。 事件 说明 $on(事件名) 事件名的类型是字符串(下同),调用它可以通过this.$on()来调用; $emit(事件名, 参数) 用于触发事件,参数是用于传递给事件的参数。这个用于触发同级事件(当前组件的) $dispatch(事件名, 参数) ①向上派发事件,用于向父组件传播。 ②会首先触发当前组件的同名事件(如果有); ③然后会向上冒泡,当遇到第一个符合的父组件的事件后触发并停止; ④当父组件的事件的返回值设为true会继续冒泡去找下一个。 $broadcast(事件名, 参数) ①向下广播事件,用于向所有子组件传播。 ②默认情况是仅限子组件; ③子组件事件的返回值是true,才会继续向该子组件的孙组件派发; ④不会触发自身同名事件; 其次,向上派发和向下广播有所区别:向上派发会触发自身同名事件,而向下广播不会; 第三,向上派发和向下广播默认只会触发直系(子或者父,不包括祖先和孙)的事件,除非事件返回值为true,才会继续在这一条线上继续。 第四,事件不能显式的通过 this.事件名 来调用它。 示例代码: <div id="app"> 父组件: <button @click="parentClick">点击向下传播broadcast</button> <br/> 子组件1: <children1></children1> <br/> 另一个子组件1: <another-children1></another-children1> </div> <script> var vm = new Vue({ el: '#app', data: { val: 1 }, methods: { parentClick: function () { this.$broadcast("parentClick", "abc"); } }, events: { childrenClick: function () { console.log("childrenClick-Parent"); }, parentClick: function () { console.log("parentClick-Parent"); return true; } }, components: { children1: { //这个无返回值,不会继续派发 template: "<button>children1</button></br>子组件2:<children2></children2>", events: { childrenClick: function () { console.log("childrenClick-children1"); }, parentClick: function (msg) { console.log("parentClick-Children1"); console.log("message:" + msg); } }, components: { children2: { template: "<button @click='findParent'>children-Click</button>", methods: { findParent: function () { this.$dispatch('childrenClick'); } }, events: { childrenClick: function () { console.log("childrenClick-children2"); }, parentClick: function (msg) { console.log("parentClick-Children2"); console.log("message:" + msg); } } } } }, anotherChildren1: { //这个是返回值为true,会继续向子组件的子组件派发 template: "<button>anotherChildren1</button></br>另一个子组件2:<another-children2></another-children2>", events: { childrenClick: function () { console.log("childrenClick-anotherChildren1"); return true; }, parentClick: function (msg) { console.log("parentClick-anotherChildren1"); console.log("message:" + msg); return true; } }, components: { anotherChildren2: { template: "<button @click='findParent'>anotherChildren2-Click</button>", methods: { findParent: function () { this.$dispatch('childrenClick'); } }, events: { childrenClick: function () { console.log("childrenClick-anotherChildren2"); }, parentClick: function (msg) { console.log("parentClick-anotherChildren2"); console.log("message:" + msg); } } } } } } });</script> }, parentClick: function () { console.log("parentClick-anotherChildren2"); } } } } } } });</script> 说明: 【1】点击父组件的按钮,会向下广播,然后触发子组件1本身,另外一个子组件1,以及另一个子组件2; 【2】点击子组件2的按钮,会触发子组件2的事件和子组件1的事件,但不会触发父组件的按钮; 【3】点击另一个子组件2的按钮,会触发另一个子组件2的事件,另一个子组件1的事件和父组件的事件(因为另一个子组件1的事件的返回值为true); ③使用v-on绑定自定义事件: 【1】简单来说,子组件触发某个事件(events里的方法)时,父组件也会执行某个方法(父组件methods里的方法)。 【2】触发的绑定写在模板之中(即被替换的那个template模板中),可以多个子组件的事件绑定一个父组件的方法,或者不同子组件的事情绑定不同父组件的方法,但是不能同一个子组件事件绑定多个父组件的方法。 【3】子组件派发消息传递的参数,即使子组件的事件没有参数,也不影响将参数传递给父组件的方法(即父组件的方法可以接受到子组件方法获取的参数) 如示例: <div id="app"> 父组件: <button>点击向下传播broadcast</button> <br/> 子组件1: <!--绑定写在这里,可以多个绑定同一个,或者不同绑定不同的,但不能一个绑定多个--> <children v-on:test="parent" @test2="another"></children> </div> <script> var vm = new Vue({ el: '#app', data: { val: 1 }, methods: { parent: function (arg) { console.log(arg); console.log("the first method with test event"); }, another: function () { console.log("another method"); } }, components: { children: { //这个无返回值,不会继续派发 template: "<button @click='childClick'>children1</button></br><button @click='childClick2'>children1</button>", methods: { childClick: function () { this.$emit("test", 'the argument for dispatch'); }, childClick2: function () { this.$emit("test2"); } }, events: { test: function () { console.log("test"); }, test2: function () { console.log("test2"); } } } } });</script> ④子组件索引 简单来说:就是可以直接从索引获取到子组件,然后就可以调用各个子组件的方法了。 添加索引方法是:在标签里添加v-ref:索引名 调用组件方法是:vm.$ref.索引名 也可以直接在父组件中使用this.$ref.索引名 这个时候,就可以获得组件了,然后通过组件可以调用他的方法,或者是使用其数据。 示例代码: <div id="app"> 父组件: <button @click="todo">触发子组件的事件</button> <br/> 子组件1: <!--绑定写在这里,可以多个绑定同一个,或者不同绑定不同的,但不能一个绑定多个--> <children v-ref:child></children> </div> <script> var vm = new Vue({ el: '#app', methods: { todo: function () { this.$refs.child.fromParent(); //通过索引调用子组件的fromParent方法 } }, components: { children: { //这个无返回值,不会继续派发 template: "<button>children1</button>", methods: { fromParent: function () { console.log("happened fromParent by ref"); } } } } });</script> (二十八)Slot分发内容 ①概述: 简单来说,假如父组件需要在子组件内放一些DOM,那么这些DOM是显示、不显示、在哪个地方显示、如何显示,就是slot分发负责的活。 ②默认情况下 父组件在子组件内套的内容,是不显示的。 例如代码: <div id="app"> <children> <span>12345</span> <!--上面这行不会显示--> </children> </div> <script> var vm = new Vue({ el: '#app', components: { children: { //这个无返回值,不会继续派发 template: "<button>为了明确作用范围,所以使用button标签</button>" } } });</script> 显示内容是一个button按钮,不包含span标签里面的内容; ③单个slot 简单来说,只使用这个标签的话,可以将父组件放在子组件的内容,放到想让他显示的地方。 <div id="app"> <children> <span>12345</span> <!--上面这行不会显示--> </children> </div> <script> var vm = new Vue({ el: '#app', components: { children: { //这个无返回值,不会继续派发 template: "<button><slot></slot>为了明确作用范围,所以使用button标签</button>" } } });</script> 例如这样写的话,结果是: <button><span>12345</span>为了明确作用范围,所以使用button标签</button> 即父组件放在子组件里的内容,插到了子组件的<slot></slot>位置; 注意,即使有多个标签,会一起被插入,相当于用父组件放在子组件里的标签,替换了<slot></slot>这个标签。 ④具名slot 将放在子组件里的不同html标签放在不同的位置 父组件在要分发的标签里添加 slot=”name名” 属性 子组件在对应分发的位置的slot标签里,添加name=”name名” 属性, 然后就会将对应的标签放在对应的位置了。 示例代码: <div id="app"> <children> <span slot="first">12345</span> <span slot="second">56789</span> <!--上面这行不会显示--> </children> </div> <script> var vm = new Vue({ el: '#app', components: { children: { //这个无返回值,不会继续派发 template: "<button><slot name='first'></slot>为了明确作用范围,<slot name='second'></slot>所以使用button标签</button>" } } });</script> 显示结果为:(为了方便查看,已手动调整换行) <button> <span slot="first">12345</span> 为了明确作用范围, <span slot="second">56789</span> 所以使用button标签 </button> ⑤分发内容的作用域: 被分发的内容的作用域,根据其所在模板决定,例如,以上标签,其在父组件的模板中(虽然其被子组件的children标签所包括,但由于他不在子组件的template属性中,因此不属于子组件),则受父组件所控制。 示例代码: <div id="app"> <children> <span slot="first" @click="tobeknow">12345</span> <span slot="second">56789</span> <!--上面这行不会显示--> </children> </div> <script> var vm = new Vue({ el: '#app', methods: { tobeknow: function () { console.log("It is the parent's method"); } }, components: { children: { //这个无返回值,不会继续派发 template: "<button><slot name='first'></slot>为了明确作用范围,<slot name='second'></slot>所以使用button标签</button>" } } });</script> 当点击文字12345的区域时(而不是按钮全部),会触发父组件的tobeknow方法。 但是点击其他区域时则没有影响。 官方教程是这么说的: 父组件模板的内容在父组件作用域内编译;子组件模板的内容在子组件作用域内编译 ⑥当没有分发内容时的提示: 假如父组件没有在子组件中放置有标签,或者是父组件在子组件中放置标签,但有slot属性,而子组件中没有该slot属性的标签。 那么,子组件的slot标签,将不会起到任何作用。 除非,该slot标签内有内容,那么在无分发内容的时候,会显示该slot标签内的内容。 如示例代码: <div id="app"> <children> <span slot="first">【12345】</span> <!--上面这行不会显示--> </children> </div> <script> var vm = new Vue({ el: '#app', components: { children: { //这个无返回值,不会继续派发 template: "<div><slot name='first'><button>【如果没有内容则显示我1】</button></slot>为了明确作用范围,<slot name='last'><button>【如果没有内容则显示我2】</button></slot>所以使用button标签</div>" } } });</script> 说明: 【1】name=’first’的slot标签被父组件对应的标签所替换(slot标签内部的内容被舍弃); 【2】name=’last’的slot标签,因为没有对应的内容,则显示该slot标签内部的内容。 ⑦假如想控制子组件根标签的属性 【1】首先,由于模板标签是属于父组件的,因此,将子组件的指令绑定在模板标签里,是不可以的(因为他归属于父组件); 【2】假如需要通过父组件控制子组件是否显示(例如v-if或者v-show),那么这个指令显然是属于父组件的(例如放在父组件的data下面)。可以将标签写在子组件的模板上。 如代码: <div id="app"> <button @click="toshow">点击让子组件显示</button> <children v-if="abc"> </children> </div> <script> var vm = new Vue({ el: '#app', data: { abc: false }, methods: { toshow: function () { this.abc = !this.abc; } }, components: { children: { //这个无返回值,不会继续派发 template: "<div>这里是子组件</div>" } } });</script> 说明: 通过父组件(点击按钮,切换v-if指令的值)控制子组件是否显示。 【3】假如需要通过子组件,控制子组件是否显示(比如让他隐藏),那么这个指令显然是属于子组件的(会将值放在子组件的data属性下),那么就不能像上面这么写,而是必须放置在子组件的根标签中。 <div id="app"> <button @click="toshow">点击让子组件显示</button> <children> <span slot="first">【12345】</span> <!--上面这行不会显示--> </children> </div> <script> var vm = new Vue({ el: '#app', methods: { toshow: function () { this.$children[0].tohidden = true; } }, components: { children: { //这个无返回值,不会继续派发 template: "<div v-if='tohidden' @click='tohide'>这里是子组件</div>", data: function () { return { tohidden: true } }, methods: { tohide: function () { this.tohidden = !this.tohidden; } } } } });</script> 说明: 点击子组件会让子组件消失; 点击父组件的按钮,通过更改子组件的tohidden属性,让子组件重新显示。 子组件的指令绑定在子组件的模板之中(如此才能调用); (二十九)组件——动态组件 ①简单来说,就是几个组件放在一个挂载点下,然后根据父组件的某个变量来决定显示哪个,或者都不显示。 ②动态切换: 在挂载点使用component标签,然后使用v-bind:is=”组件名”,会自动去找匹配的组件名,如果没有,则不显示; 改变挂载的组件,只需要修改is指令的值即可。 如示例代码: <div id="app"> <button @click="toshow">点击让子组件显示</button> <component v-bind:is="which_to_show"></component> </div> <script> var vm = new Vue({ el: '#app', data: { which_to_show: "first" }, methods: { toshow: function () { //切换组件显示 var arr = ["first", "second", "third", ""]; var index = arr.indexOf(this.which_to_show); if (index < 3) { this.which_to_show = arr[index + 1]; } else { this.which_to_show = arr[0]; } } }, components: { first: { //第一个子组件 template: "<div>这里是子组件1</div>" }, second: { //第二个子组件 template: "<div>这里是子组件2</div>" }, third: { //第三个子组件 template: "<div>这里是子组件3</div>" }, } });</script> 说明: 点击父组件的按钮,会自动切换显示某一个子组件(根据which_to_show这个变量的值来决定)。 ③keep-alive 简单来说,被切换掉(非当前显示)的组件,是直接被移除了。 在父组件中查看this.$children属性,可以发现,当子组件存在时,该属性的length为1,而子组件不存在时,该属性的length是0(无法获取到子组件); 假如需要子组件在切换后,依然需要他保留在内存中,避免下次出现的时候重新渲染。那么就应该在component标签中添加keep-alive属性。 如代码: <div id="app"> <button @click="toshow">点击让子组件显示</button> <component v-bind:is="which_to_show" keep-alive></component> </div> <script> var vm = new Vue({ el: '#app', data: { which_to_show: "first" }, methods: { toshow: function () { //切换组件显示 var arr = ["first", "second", "third", ""]; var index = arr.indexOf(this.which_to_show); if (index < 3) { this.which_to_show = arr[index + 1]; } else { this.which_to_show = arr[0]; } console.log(this.$children); } }, components: { first: { //第一个子组件 template: "<div>这里是子组件1</div>" }, second: { //第二个子组件 template: "<div>这里是子组件2</div>" }, third: { //第三个子组件 template: "<div>这里是子组件3</div>" }, } });</script> 说明: 初始情况下,vm.$children属性中只有一个元素(first组件), 点击按钮切换后,vm.$children属性中有两个元素, 再次切换后,则有三个元素(三个子组件都保留在内存中)。 之后无论如何切换,将一直保持有三个元素。 ④activate钩子 简单来说,他是延迟加载。 例如,在发起ajax请求时,会需要等待一些时间,假如我们需要在ajax请求完成后,再进行加载,那么就需要用到activate钩子了。 具体用法来说,activate是和template、data等属性平级的一个属性,形式是一个函数,函数里默认有一个参数,而这个参数是一个函数,执行这个函数时,才会切换组件。 为了证明他的延迟加载性,在服务器端我设置当发起某个ajax请求时,会延迟2秒才返回内容,因此,第一次切换组件2时,需要等待2秒才会成功切换: <div id="app"> <button @click="toshow">点击让子组件显示</button> <component v-bind:is="which_to_show"></component> </div> <script> var vm = new Vue({ el: '#app', data: { which_to_show: "first" }, methods: { toshow: function () { //切换组件显示 var arr = ["first", "second", "third", ""]; var index = arr.indexOf(this.which_to_show); if (index < 3) { this.which_to_show = arr[index + 1]; } else { this.which_to_show = arr[0]; } console.log(this.$children); } }, components: { first: { //第一个子组件 template: "<div>这里是子组件1</div>" }, second: { //第二个子组件 template: "<div>这里是子组件2,这里是ajax后的内容:{{hello}}</div>", data: function () { return { hello: "" } }, activate: function (done) { //执行这个参数时,才会切换组件 var self = this; $.get("/test", function (data) { //这个ajax我手动在服务器端设置延迟为2000ms,因此需要等待2秒后才会切换 self.hello = data; done(); //ajax执行成功,切换组件 }) } }, third: { //第三个子组件 template: "<div>这里是子组件3</div>" } } });</script> 代码效果: 【1】第一次切换到组件2时,需要等待2秒后才能显示(因为发起ajax); 【2】在有keep-alive的情况下,第二次或之后切换到组件2时,无需等待;但ajax内容,需要在第一次发起ajax两秒后才会显示; 【3】在无keep-alive的情况下(切换掉后没有保存在内存中),第二次切换到组件2时,依然需要等待。 【4】等待时,不影响再次切换(即等待组件2的时候,再次点击切换,可以直接切换到组件3); 说明: 【1】只有在第一次渲染组件时,才会执行activate,且该函数只会执行一次(在第一次组件出现的时候延迟组件出现) 【2】没有keep-alive时,每次切换组件出现都是重新渲染(因为之前隐藏时执行了destroy过程),因此会执行activate方法。 ⑤transition-mode过渡模式 简单来说,动态组件切换时,让其出现动画效果。(还记不记得在过渡那一节的说明,过渡适用于动态组件) 默认是进入和退出一起完成;(可能造成进入的内容出现在退出内容的下方,这个下方指y轴方面偏下的,等退出完毕后,进入的才会出现在正确的位置); transition-mode=”out-in”时,动画是先出后进; transition-mode=”in-out”时,动画是先进后出(同默认情况容易出现的问题); 示例代码:(使用自定义过渡名和animate.css文件) <div id="app"> <button @click="toshow">点击让子组件显示</button> <component v-bind:is="which_to_show" class="animated" transition="bounce" transition-mode="out-in"></component> </div> <script> Vue.transition("bounce", { enterClass: 'bounceInLeft', leaveClass: 'bounceOutRight' }) var vm = new Vue({ el: '#app', data: { which_to_show: "first" }, methods: { toshow: function () { //切换组件显示 var arr = ["first", "second", "third", ""]; var index = arr.indexOf(this.which_to_show); if (index < 3) { this.which_to_show = arr[index + 1]; } else { this.which_to_show = arr[0]; } } }, components: { first: { //第一个子组件 template: "<div>这里是子组件1</div>" }, second: { //第二个子组件 template: "<div>这里是子组件2,这里是ajax后的内容:{{hello}}</div>", data: function () { return { hello: "" } } }, third: { //第三个子组件 template: "<div>这里是子组件3</div>" } } });</script> (三十)组件——杂项 ①组件和v-for 简单来说,就是组件被多次复用; 例如表格里的某一行,又例如电商的商品橱窗展示(单个橱窗),都可以成为可以被复用的组件; 只要编写其中一个作为组件,然后使数据来源成为一个数组(或对象,但个人觉得最好是数组),通过v-for的遍历,组件的每个实例,都可以获取这个数组中的一项,从而生成全部的组件。 而数据传输,由于复用,所以需要使用props,将遍历结果i,和props绑定的数据绑定起来,绑定方法同普通的形式,在模板中绑定。 示例代码: <div id="app"> <button @click="toknowchildren">点击让子组件显示</button> <table> <tr> <td>索引</td> <td>ID</td> <td>说明</td> </tr> <tr is="the-tr" v-for="i in items" v-bind:id="i" :index="$index"></tr> </table> </div> <script> var vm = new Vue({ el: '#app', data: { items: [1, 2, 3, 4] }, methods: { toknowchildren: function () { //切换组件显示 console.log(this.$children); } }, components: { theTr: { //第一个子组件 template: "<tr>" + "<td>{{index}}</td>" + "<td>{{id}}</td>" + "<td>这里是子组件</td>" + "</tr>", props: ['id','index'] } } });</script> 说明: 【1】记得将要传递的数据放在props里! 【2】将index和索引$index绑定起来,因为索引从0开始,因此索引所在列是从0开始;id是和遍历items的i绑定在一起的,因此id从1开始。 【3】可以在父组件中,通过this.$children来获取子组件(但是比较麻烦,特别是组件多的时候,比较难定位); ②编写可复用的组件: 简单来说,一次性组件(只用在这里,不会被复用的)跟其他组件紧密耦合是可以的,但是,可复用的组件应当定义一个清晰的公开接口。(不然别人怎么用?) 可复用的组件,基本都是要和外部交互的,而一个组件和外部公开的交互接口有: 【1】props:允许外部环境数据传递给组件; 【2】事件:允许组件触发外部环境的action,就是说通过在挂载点添加v-on指令,让子组件的events触发时,同时触发父组件的methods; 【3】slot:分发,允许将父组件的内容插入到子组件的视图结构中。 如代码: <div id="app"> <p>这是第一个父组件</p> <widget :the-value="test" @some="todo"> <span>【第一个父组件插入的内容】</span> </widget> </div> <div id="app2"> <p>这是第二个父组件</p> <widget @some="todo"> </widget> </div> <script> Vue.component("widget", { template: "<button @click='dosomething'><slot></slot>这是一个复用的组件,点击他{{theValue}}</button>", methods: { dosomething: function () { this.$emit("some"); } }, events: { some: function () { console.log("widget click"); } }, props: ['theValue'] }) var vm = new Vue({ el: '#app', data: { test: "test" }, methods: { todo: function () { console.log("这是第一个父组件") } } }); var vm_other = new Vue({ el: '#app2', data: { name: "first" }, methods: { todo: function () { console.log("这是另外一个父组件") } } });</script> 说明: 【1】在第一个父组件中使用了分发slot,使用了props来传递值(将test的值传到子组件的theValue之中); 【2】在两个组件中,子组件在点击后,调用methods里的dosomething方法,然后执行了events里的some事件。又通过挂载点的@some=”todo”,将子组件的some事件和父组件的todo方法绑定在一起。 因此,点击子组件后,最终会执行父组件的todo方法。 【3】更改父组件中,被传递到子组件的值,会同步更改子组件的值(即二者会数据绑定); ③异步组件: 按照我的理解,简单来说,一个大型应用,他有多个组件,但有些组件无需立即加载,因此被分拆成多个组件(比如说需要立即加载的,不需要立即加载的); 需要立即加载的,显然放在同一个文件中比较好(或者同一批一起请求); 而不需要立即加载的,可以放在其他文件中,但需要的时候,再ajax向服务器请求; 这些后续请求的呢,就是异步组件; 做到这种异步功能的,就是Vue.js的功能——允许将组件定义为一个工厂函数,动态解析组件的定义。 可以配合webpack使用。 至于如何具体使用,我还不太明白,教程中写的不清,先搁置等需要的时候来研究。 链接: http://cn.vuejs.org/guide/components.html#u5F02_u6B65_u7EC4_u4EF6 ④资源命名的约定: 简单来说,html标签(比如div和DIV是一样的)和特性(比如要写成v-on这样的指令而不是vOn)是不区分大小写的。 而资源名往往是写成驼峰式(比如camelCase驼峰式),或者单词首字母都大写的形式(比如PascalCase,我不知道该怎么称呼这个,不过这样写很少的说)。 Vue.component("myTemplate", { //......略}) Vue.js可以自动识别这个并转换, <my-template></my-template> 以上那个模板可以自动替换这个标签。 ⑤递归组件: 简单来说,递归组件就是组件在自己里内嵌自己的模板。 组件想要递归,需要name属性,而Vue.component自带name属性。 大概样子是这样的, <div id="app"> <my-template></my-template> </div> <script> Vue.component("myTemplate", { template: "<p><my-template></my-template></p>" }) 这种是无限递归,肯定是不行的。因此,需要控制他递归的层数,例如通过数据来控制递归,当数据为空时,则停止递归。 示例代码如下: <ul id="app"> <li> {{b}} </li> <my-template v-if="a" :a="a.a" :b="a.b"></my-template> </ul> <script> Vue.component("myTemplate", { template: '<ul><li>{{b}}</li><my-template v-if="a" :a="a.a" :b="a.b"></my-template></ul>', props: ["a", "b"] }) var data = { a: { a: { a: 0, b: 3 }, b: 2 }, b: 1 } var vm = new Vue({ el: '#app', data: data, methods: { todo: function () { this.test += "!"; console.log(this.test); } } });</script> 说明: 【1】向下传递时,通过props传递a的值和b的值,其中a的值作为递归后组件的a和b的值的数据来源; 然后判断传递到递归后的组件的a的值是否存在,如果存在则继续递归; 如果a的值不存在,则停止递归。 ⑥片断实例: 简单来说,所谓片断实例,就是组件的模板不是处于一个根节点之下: 片断实例代码: Vue.component("myTemplate", { template: '<div>1</div>' + '<div>2</div>', }) 非片断实例: Vue.component("myTemplate", { template: '<div>' + '<div>1</div>' + '<div>2</div>' + '</div>', }) 片断实例的以下特性被忽略: 【1】组件元素上的非流程控制指令(例如写在挂载点上的,由父组件控制的v-show指令之类,但注意,v-if属于流程控制指令); 【2】非props特性(注意,props不会被忽略,另外props是写在挂载点上的); 【3】过渡(就是transition这个属性,将被忽略); 更多的参照官方文档: http://cn.vuejs.org/guide/components.html#u7247_u65AD_u5B9E_u4F8B ⑦内联模板 参照:http://cn.vuejs.org/guide/components.html#u5185_u8054_u6A21_u677F 反正我试了下失败了,google也没搜到相关的内容,┑( ̄Д  ̄)┍ ————————组件的基本知识到这里结束————————————
本篇资料来于官方文档: http://cn.vuejs.org/guide/transitions.html 本文是在官方文档的基础上,更加细致的说明,代码更多更全。 简单来说,更适合新手阅读 (二十四)过渡动画 ①过渡动画的定义; 简单来说,就是当模块消失、出现时,会以什么样的形式消失和出现; 如果要使用过渡动画,则在标签里加入属性: transition=”过渡动画名” 例如: <div class="box" v-if="box_1" transition="mytran">1</div> 这里是mytran就是过渡动画名,他是一个类名,动画将基于这个名字而添加多个不同的扩展名(具体请参看下面 ②过渡动画绑定的事件: 【1】v-if 【2】v-show 【3】v-for(只在插入和删除时触发,可以自己写,或者使用vue-animated-list插件); 自己写例如: <div v-for="i in items" class="box" transition="mytran">{{i}}</div> 动画略 【4】动态组件; 【5】在组件的根节点上,并且被Vue实例DOM方法触发(例如:vm.$appendTo(el))。大概就是说,把组件添加到某个根节点上去。 ③CSS动画: 【1】首先,需要有transition属性,然后取得其值; 【2】其次,CSS里需要有以值为名的三个类名,分别是: 假设transition的值为mytran,则类名为 说明 .mytran-transition 动画状态,css的transition属性放在这里,他表示的类会始终存在于DOM之上; 另外这里的样式会覆盖标签的默认class提供的样式 .mytran-enter 进入时,组件从这个css状态扩展为当前css状态,这个类只存在最开始的一帧 .mytran-leave 退出时,组件从原来的css状态恢复为这个状态,这个类从退出开始时生效,在退出结束时删除。 如代码: <style> .box { width: 100px; height: 100px; border: 1px solid red; display: inline-block; } /*这个定义动画情况,以及存在时的样式,这个样式会覆盖class里的样式*/ .mytran-transition { transition: all 0.3s ease; background-color: greenyellow; } /* .mytran-enter 定义进入的开始状态 */ /* .mytran-leave 定义离开的结束状态 */ .mytran-enter, .mytran-leave { height: 0; width: 0; }</style> <div id="app"> <button @click="change">点击随机隐藏和显示</button> <br/> <div class="box" v-if="box_1" transition="mytran">1</div> <div class="box" v-if="box_2" transition="mytran">2</div> <div class="box" v-if="box_3" transition="mytran">3</div> </div> <script> var vm = new Vue({ el: '#app', data: { box_1: true, box_2: true, box_3: true }, methods: { change: function () { for (var i = 1; i < 4; i++) { this['box_' + i] = Math.random() > 0.5 ? true : false; } } } }) setInterval(vm.change, 300);</script> 点击会随机让3个方块隐藏或者显示; ④JavaScript钩子: 【1】简单来说,这个不影响CSS动画(依然是那三个类的变化); 【2】这个主要用于抓取进入和离开各四个时刻,用于做某些事情; 【3】这八个时刻分别为: 进入:beforeEnter(进入之前),enter(进入动画刚开始),afterEnter(进入动画结束),enterCancelled(进入被中断); 退出:beforeLeave(退出之前),leave(退出动画刚开始),afterLeave(退出动画结束),leaveCancelled(退出被中断); 【4】对DOM的修改,部分情况下会恢复,例如在leave这一步修改dom的textContent属性,将在dom重新进入时恢复原状;但若在enter这一步修改,则不会恢复。 如代码: Vue.transition('mytran', { beforeEnter: function (el) { //进入之前 console.log("进入动画开始时间:" + new Date().getTime()); }, enter: function (el) { el.textContent = new Date(); }, afterEnter: function (el) { console.log("进入结束时间:" + new Date().getTime()); }, beforeLeave: function (el) { console.log("离开动画开始时间:" + new Date().getTime()); }, leave: function (el) { $(el).text("离开中..." + new Date()); }, afterLeave: function (el) { console.log("离开结束时间:" + new Date().getTime()); } }) ⑤自定义过渡类名: 之所以要自定义过渡类名,是因为我们不可能要求每个css动画的样式,都是按照Vuejs标准的写法来写的(比如我们下载别人写的代码); 注:需要在声明相关的Vue实例之前进行定义。 首先,推荐一个Vuejs官方教程推荐的动画集合: https://daneden.github.io/animate.css/ 下载后,导入这个css文件,然后开始自定义动画; <div id="app"> <button @click="change">点击随机隐藏和显示</button> <br/> <div class="box animated" v-if="box" transition="bounce">1</div> </div> <script> Vue.transition("bounce", { enterClass: 'bounceInLeft', leaveClass: 'bounceOutRight' }) var vm = new Vue({ el: '#app', data: { box: true }, methods: { change: function () { this.box = !this.box; } } });</script> 解释: 【1】进行动画的标签,需要有animated这个class; 【2】enterClass和leaveClass相当于之前的xxx-enter和xxx-leave; 【3】效果是从左边闪进来,从右边闪出去。 【4】需要在声明Vue实例前设置动画,否则会无效; ⑥使用animation动画 在Vuejs中,animation动画和transition动画是不同的。 简单来说,transition动画分为三步:常驻类,进入时触发的类,退出时触发的类;只有常驻类有transition动画属性,其他两步只有css状态; 而animation动画分为两步:进入时触发的类,退出时触发的类。当然,还有xxx-transition这个类存在于dom之中(这个类可以用于设置动画原点,或者干脆不设置这个类); 在animation动画中,进入和退出时的class类,都应该有动画效果,例如: @keyframes fat { 0% { width: 100px } 50% { width: 200px } 100% { width: 100px } } .fat-leave, .fat-enter { animation: fat 1s both;} 进入和退出时,执行的类名和transition一样,都是xxx-leave和xxx-enter这样格式的; 当然,也可以自定义类名。 示例代码: <style> .box { width: 100px; height: 100px; border: 1px solid red; display: inline-block; } @keyframes fat { 0% { width: 100px } 50% { width: 200px } 100% { width: 100px } } .fat-leave, .fat-enter { animation: fat 1s both; }</style> <div id="app"> <button @click="change">点击随机隐藏和显示</button> <br/> <div class="box animated" v-if="box" transition="fat">1</div> </div> <script> var vm = new Vue({ el: '#app', data: { box: true }, methods: { change: function () { this.box = !this.box; } } });</script> 效果: 消失:先变宽,再恢复,然后消失; 进入:出现,变宽,再恢复,停留; 这里偷懒共用了同一个动画效果。 ⑦除此之外,还有 【1】显式声明动画类型(假如动画同时存在transition和animation,且分情况执行其中一种); 【2】过渡流程详解(触发动画时,js钩子执行与css执行的顺序); 【3】CSS动画(就是animation,像上面那样已经写过了,具体略); 【4】JavaScript过渡(不是js钩子,钩子是指在某个阶段会调用某个函数,但这个钩子跟动画无关),用JavaScript来控制动画,比如jquery的animate方法; 【5】v-for使用的渐进过渡; 由于暂时用不上,所以略掉,需要查看的请打开连接: http://cn.vuejs.org/guide/transitions.html
资料来于官方文档: http://cn.vuejs.org/guide/forms.html 本文是在官方文档的基础上,更加细致的说明,代码更多更全。 简单来说,更适合新手阅读 (二十三)表单绑定 ①常见绑定方法: 【1】文本输入框绑定; 【2】textarea绑定(类似【1】); 【3】radio选中值绑定; 【4】checkbox绑定(自动捆绑数组,无需name); 【5】select绑定; 【6】select multiple多选选中框绑定; 【7】动态绑定(以上不同类型但同一个值可以互动); 【8】checkbox选中和未选中赋予不同的值(主要是针对不选中状态); 【9】checkbox,radio,select选中状态的值动态绑定(主要是指值动态绑定对象或者是vm实例的属性,例如data里的某个属性,或者是computed的某个值); 如代码: <div id="app"> <input type="text" v-model="text"/> <div>{{text}}</div> <div>——————————————</div> <textarea style="width:200px;height:100px;" v-model="textarea"></textarea> <div>{{textarea}}</div> <div>——————————————</div> <label><input type="checkbox" v-model="checkbox"/>左边选中右边则为true:{{checkbox}}</label> <div>——————————————</div> <label><input type="checkbox" value="firstCheckbox" v-model="checkboxes">firstCheckbox</label><br/> <label><input type="checkbox" value="secondCheckbox" v-model="checkboxes">secondCheckbox</label><br/> <label><input type="checkbox" value="thirdCheckbox" v-model="checkboxes">thirdCheckbox</label><br/> <div>以上选中的value情况为:{{checkboxes}}</div> <div>以上选中的value情况为(以json格式显示,这里使用了json过滤器):{{checkboxes|json}}</div> <div>——————————————</div> <label><input type="radio" value="A" v-model="radio"/>value = A</label><br> <label><input type="radio" value="B" v-model="radio"/>value = B</label><br> <div>{{radio}}</div> <div>注意,这里的v-model的值应该被注册到data里面,否则会红字警告(事实上,所有的都应该也这么做)</div> <div>——————————————</div> <select v-model="select"> <option>默认值,option不设value</option> <option value="B">value的值设为B</option> <option selected value="C">默认选择这个,value设为C</option> </select> <div>{{select}}</div> <div>同样,这里不注册也会被报错</div> <div>——————————————</div> <div>以下是select的多选,按住ctrl可以连续选,按住shift选择区间</div> <select style="width:200px;height:100px;overflow: hidden;" v-model="multiple" multiple> <option value="A">A</option> <option value="B">B</option> <option value="C">C</option> <option value="D">D</option> <option value="E">E</option> </select> <div>多选选中的值是:{{multiple}}</div> <div>注意,这个多选select框是默认带y轴的滚动条的</div> <div>——————————————</div> <div>动态渲染,checkbox和多选select框是互相影响的</div> <label><input type="checkbox" value="A" v-model="Dynamic">A</label><br/> <label><input type="checkbox" value="B" v-model="Dynamic">B</label><br/> <label><input type="checkbox" value="C" v-model="Dynamic">C</label><br/> <select style="width:200px;height:100px;overflow: hidden;" v-model="Dynamic" multiple> <option value="A">A</option> <option value="B">B</option> <option value="C">C</option> </select> <div>选中情况是:{{Dynamic}}</div> <div>——————————————</div> <div>选中和选中的值自定义的checkbox</div> <label><input type="checkbox" v-bind:true-value="differentValues.t" v-bind:false-value="differentValues.f" v-model="different">true/false</label><br/> <div>different value: {{different}}</div> <div>注意,以上不能像普通checkbox那么样,用一个数组作为多个checkbox的v-model的变量,且其值是绑定与vm实例的某个属性; 因此,不能在v-bind里的值是一个字符串,但可以是一个对象,例如{a:1}这样(当然,这个时候显示的值也是一个对象了) </div> <div>——————————————</div> <div>自定义之的radio</div> <label><input type="radio" v-bind:value="text" v-model="customize"/>值为1</label> <label><input type="radio" v-bind:value="textarea" v-model="customize"/>值为1</label> <div>{{customize}}</div> <div>同样,值可以是vm的一个属性或者是一个对象,另外,同样有效的还有select。(主要就这三个有选中状态,除此之外虽然例如Date类型也有选中,但是意义不大)</div> <div>——————————————</div> </div> <script> var vm = new Vue({ el: '#app', data: { text: "默认有输入内容", textarea: "这里是多行文字\n第二行,\\n是换行符,但在字符串里显示为空格", checkboxes: [], radio: "", select: "", multiple: "", Dynamic: {}, different: "", differentValues: { t: "true", f: "false" }, customize: '' } }) </script> ②添加参数: 参数 说明 lazy 非实时更新,而是focus状态结束后更新 number 将值自动转为number类型输出 debounce 延迟若干毫秒再更新数值 【1】lazy 在取消focus状态后才更新值,而不是按键按下时就更新值。 对文本输入框和textarea都有效 如代码: <input type="text" v-model="text" lazy/> <div>{{text}}</div> 【2】number 将输入的值自动转为number类型,如果转后为NaN类型,则返回原值; 如代码: <input type="text" v-model="text" number/> <div>{{text}}</div> <div>{{typeof text}}</div> 如果加上number这个参数,那么输入数字时,则提示类型为string,加上之后,纯数字会提示number类型; 【3】debounce=”毫秒数” 当值连续若干毫秒没有变化时,触发变量的值的改变; 如代码: <input type="text" v-model="text" debounce="1000"/> <div>{{text}}</div> 当我以500ms的时间差依次输入1,2,3,4,5,6这六个数字时,text的值不会被更新; 当我停止输入有1000ms时,text值才会被更新; 因此,适合类似ajax等高消耗操作。
资料来于官方文档: http://cn.vuejs.org/guide/events.html 本文是在官方文档的基础上,更加细致的说明,代码更多更全。 简单来说,更适合新手阅读 (二十二)方法处理器 ①v-on的标准用法 用于监听DOM事件,典型的就是v-on:click,处理的方法放在methods属性里 会默认传一个参数,代码如下: <button @click="test">点击</button> methods: { test: function (evt) { console.log(evt); } } 这里的evt,是标准的鼠标点击事件,类似jquery的click事件的回调函数中的参数。 可以通过this来找到data属性里的值(或许也能找到其他几个),例如: data: { items: "test"},methods: { test: function (evt) { console.log(this.items); console.log(evt); } } 这里的this.items,就是data的items这个变量; ②内联语句处理器 给v-on事件传一个固定参数 <button @click="test('a')">点击搜索age</button> 当这个时候,函数的第一个参数就不是鼠标点击事件了,而是字符串a test: function (mes) { console.log(mes); } mes的值是’a’ $event 如果需要给他一个像上面一样的鼠标点击事件时,则使用$event作为参数(他和不传参数时的默认鼠标事件对象是相同的); 使用Vue实例的变量 如果需要传一个data属性里的值,则直接放属性名 例如: <div id="app"> <a href="http://www.baidu.com" @click="test(items, $event)">点击搜索age</a> </div> <script> var test = {name: "test"}; var vm = new Vue({ el: '#app', data: { items: "test" }, methods: { test: function (msg, evt) { console.log(msg); evt.preventDefault();//阻止默认动作,比如这里是页面跳转 } } })</script> 输出:test和BUTTON ③事件修饰符(针对v-on) 修饰符 效果 备注 .prevent 阻止html元素的默认事件 类似evt.preventDefault() .stop 阻止事件冒泡 keyup.数字 当该数字表示的按键弹起时 有别名 keyup.enter 回车 按下回车时 keyup.tab Tab按钮 tab切入该input时 keyup.delete del键 会导致原始的delete删除功能失效 keyup.esc esc键 按下esc时 keyup.space 空格键 不会使空格功能失效(即按下空格时,既空格,又触发事件) keyup.up 键盘方向键的上 上键同时会使光标到输入框的最左边 (焦点在输入框时才生效,按键弹起时生效,下同) keyup.down 键盘方向键的下 到输入框的最后面 keyup.left 方向左键 光标左移 keyup.right 方向右键 光标右移 .self 当前元素本身(非子元素)时触发事件 类似冒泡的时候找最顶层,一般用于click之类的鼠标事件(1.0.16之后) .capture 按照capture模式来处理 简单来说,根据我推测,是根据捕获顺序触发冒泡(原本模式是后捕获先冒泡,这个正好相反)(1.0.16之后) 对于.self来说,例如以下代码: <div id="app"> <div @click.self="test" class="a"> <div class="b"> </div> </div> </div> <script> var test = {name: "test"}; var vm = new Vue({ el: '#app', data: { items: "test" }, methods: { test: function (evt) { console.log(evt); } } })</script> 只有当点击到非div class=’b’的区域时,才会触发事件; ④自定义按键别名: 规范: Vue.directive(“on”),keyCode.别名 = 按键码; 示例: Vue.directive("on").keyCode.z = 122; 这个指键盘码为122(小写z)的别名命名为z,当按键键盘的z键时(无论大小写),都会触发事件。 注意,这个要写在实例声明之后(推测是要含有该按键的template被创建后才有效)
(二十)v-if ①简单来说,该值为true则显示该标签,为false则不显示; 如例: <div id="app"> <div v-if="abc">{{abc.a}}</div> </div> <script> var vm = new Vue({ el: '#app', data: { abc: { a: "1" } } })</script> 当abc这个对象存在时,显示这一行数据,其内容为abc.a的值; 假如abc这个对象不存在,那么则不显示; 也可以用另外一个变量来控制其是否显示(能否显示决定于该值隐式转换为boolean类型时是true还是false); 例如假如上面有abc这个对象,但这个对象是空对象(没有属性a),但空对象隐式转换后为true,因此会有div,但这个div里没有内容; ②template v-if 包装以同时影响多个html标签; 即假如多个标签(且他们是连续的),被一个变量控制是否显示,那么每个都这么写显然太繁琐,因此用一个template标签将这些标签包裹起来,用v-if标签控制该template标签是否显示,实际渲染时,template标签不会显示,只会显示其内的标签; 如示例: <div id="app"> <template v-if="abc"> <div>{{abc[0]}}</div> <div>{{abc[1]}}</div> <div>{{abc[2]}}</div> </template> </div> <script> var vm = new Vue({ el: '#app', data: { abc: [1, 2, 3] } })</script> 由于非空数组是值为true,空数组的值为false,因此方便控制; 另外,这里只是演示,事实上更好的写法的v-for来控制内部三个标签来同时显示(当然,如果不需要显示全部的则不应该这么写); 显示内容: <div id="app"> <div>1</div> <div>2</div> <div>3</div> </div> ③v-show 用于控制该标签的display样式 他的特点是,dom存在于页面内,已经渲染、事件绑定完毕,区别只是是否显示。 例如: <div v-show="a">{{test}}</div> a的值为true,则正常显示; a的值为false,则自动添加display:none v-show不支持template写法(即不能同时控制多个同级连续div); ④v-else v-if和v-show的补充语句; 即v-if和v-show的判断为true时,不显示v-else标签的内容;否则显示v-else标签的内容。 例如: <div id="app"> <div v-show="a">{{test}}</div> <div v-else>def</div> </div> <script> var vm = new Vue({ el: '#app', data: { a: true, test: "abc" } })</script> 显示abc; 若把data中的a改为false,则显示def; 另外,标签之间需要连续,如以下,v-else则不能正常生效: <div v-show="a">{{test}}</div> <div>another</div> <div v-else>def</div> 另外,不要在组件的情况下使用v-else,而是采用v-show=”!变量名”来变相起到v-else的效果 ⑤按照说明 v-if v-show 渲染时间 第一次为真时 刚开始就渲染 切换形式 动态生成,局部编译/卸载 控制display属性 生成消耗 较小(只生成为真的部分) 较大(生成全部) 切换消耗 较大(切换时需要局部编译) 较小(因为生成时已经渲染完成) v-if是在第一次条件为真时,进行渲染(比如他下面还有其他组件); v-show因为只是控制display的属性,因此开始就会渲染; (二十一)v-for列表渲染 ①标准写法 <li v-for="i in items">{{i}}</li> 【1】items是一个对象或者数组; 【2】该格式相当于for(var i in items){//略} 【3】插值的i相当于items[i] 【4】该li会被复制多个,然后依次被items[i]渲染,直到渲染完毕; 示例: <div id="app"> <ul> <li v-for="i in items">{{i}}</li> </ul> </div> <script> var vm = new Vue({ el: '#app', data: { items: { a: "1", b: "2", c: "3" } } })</script> 结果: <div id="app"> <ul> <li>1</li> <li>2</li> <li>3</li> </ul> </div> ②索引编号: 在标签里使用$index,即表示当前索引在v-for中的编号(从0开始); 例如上面改为: <li v-for="i in items">{{i}}'s index is {{$index}}</li> 显示的是从0~2 ③template v-for 用于包裹多个标签的v-for 简单来说,需要将多个标签都用v-for遍历,那么就需要用template标签。 同样,template在实际渲染的时候不会出现,只是起到一个包裹作用。 如代码: <div id="app"> <ul> <template v-for="i in items"> <li>Index is {{$index}}</li> <li>Content is {{i}}</li> </template> </ul> </div> <script> var vm = new Vue({ el: '#app', data: { items: { a: "1", b: "2", c: "3" } } })</script> 显示结果是: <ul> <li>Index is 0</li><li>Content is 1</li> <li>Index is 1</li><li>Content is 2</li> <li>Index is 2</li><li>Content is 3</li> </ul> ④监视数组变动(修改数组) 当data的某个属性是一个数组时,用v-for可以遍历,但显然数组是可能变动的,因此对以下变动也进行数据绑定; push() 数组末尾添加 pop() 数组末尾取出 shift() 数组开头取出 unshift() 数组开头添加 splice() 删除并插入 sort() 排序 reverse() 数组顺序颠倒 当利用以上方法变更数组时,被渲染的内容会实时更新; ⑤监视数组的改变(用另一个数组进行替换) 但数组从一个数组变为另一个数组时(记得,数组是按引用传递的),数据绑定依然生效; 但前提是使用以下方法: filter() 过滤,参数是一个函数,取其返回值为true的元素被添加到新数组 concat() 合并两个数组,返回的数组是合并后的 slice() 返回数组的拷贝,从开始索引到结束索引(前含后不含) ⑥track-by 按照说明,假如用一个新的对象数组来替换已有的对象数组(并且两个对象数组其对象的属性不同),那么由于v-for默认是通过数据对象的特征来决定已有作用域和DOM元素的复用程度,可能导致重新渲染整个列表(比如列表很大的话可能会导致效率很低)。 按照我的理解,大概就是从这样一个数组:(我推测是这样的) items: [ {name: "a", age: 10}, {name: "b", age: 11}, {name: "c", age: 12} ] 变成这样 items = [ {name: "A", height: 150}, {name: "B", height: 160}, {name: "C", height: 170} ] 会导致列表重新渲染,如果列表内容特别多,那么就可能带来影响效率和性能。 解决这种方法的办法,就是使用track-by,即加入一个包含某特定属性的标识符,当这个属性的标识符其值相等时,则尽可能复用这个已有对象的作用域和DOM元素。 例如标签写法如下: <li v-for="item in items" track-by="id">{{item.name}}</li> 数据从: items: [ {id: 1, name: "a", age: 10}, {id: 2, name: "b", age: 11}, {id: 3, name: "c", age: 12} ] 变为: items = [ {id: 3, name: "A", height: 150}, {id: 2, name: "B", height: 160}, {id: 1, name: "C", height: 170} ] 那么在替换时,会复用。 注意,复用并不是使用之前同id的对象所在的dom位置,例如并不会将name=C的元素放在原有的name=a的元素的位置(即使他们id都为1),而是依然根据数组下标的顺序来显示数据。 ⑦track-by=”$index” 当数组没有某一个共同属性时(比如上面的id),但依然需要复用的话,那么就用这个。 他会强制v-for进入原位更新模式,片断不会被移动(我推测这个片断指v-for的dom标签),简单的根据对应的索引的值来刷新dom,这种模式可以处理数组中重复的值。(应该是指对于重复的值,比如像上面那种id为同一个数值的,也可以正常复用)。 由于替换方式简单暴力(索引肯定是一样的),所以高效。 ———————————————————————— 这让数据替换非常高效,但是也会付出一定的代价。因为这时 DOM 节点不再映射数组元素顺序的改变,不能同步临时状态(比如 <input> 元素的值)以及组件的私有状态。因此,如果 v-for 块包含 <input> 元素或子组件,要小心使用 track-by="$index" ——————我表示以上这段话没看懂—————— 如以下代码,reverse()依然在起作用啊 <div id="app"> <ul> <li v-for="item in items" track-by="$index">{{item.name}}</li> </ul> <button onclick="chagne()">change</button> </div> <script> var vm = new Vue({ el: '#app', data: { items: [ {id: 1, name: "a", age: 10}, {id: 2, name: "b", age: 11}, {id: 3, name: "c", age: 12} ] } }) function chagne() { vm.items = [ {id: 3, name: "A", height: 150}, {id: 2, name: "B", height: 160}, {id: 1, name: "C", height: 170} ]; vm.items.reverse(); }</script> ⑧数组的一些方法: $set(索引, 被替换的值) 简单来说,以下代码是不会触发数据绑定的: vm.items[0] = {name: "test"}; 替代方法是: vm.items.$set(0, {name: "test"}); $remove(被移除的对象) 假如要移除某个对象(注意,由于对象是按引用传递,因此不能简单用看起来一样的对象来移除某个对象),可以直接使用这个方法。具体代码是: <script> var test = {name: "test"}; var vm = new Vue({ el: '#app', data: { items: [ {name: "a"}, {name: "b"}, {name: "c"} ] } }) vm.items.push(test); function chagne() { vm.items.$remove(test); //vm.items.$remove({name: "test"}); //注意,这种写法是错误的 }</script> 他相当于先用indexOf找到该对象的索引,再用splice来从数组中移除该对象。 Object.freeze(数组的对象元素) 假如数组中某一个元素(他是个对象)被Object.freeze冻结了,需要明确指定track-by,这种情况下,如果Vuejs不能自动追踪对象,将给出一条警告。 ——不懂!—— 反正被这样搞的对象,其值不能被修改,修改其值也没用(修改无效) ⑨$key用于获取被遍历对象的key值 即js代码中,for(var i in items)中的i,记得,在v-for里,其是items[i] 但仅对object对象生效,对数组无效(数组可以使用$index) 如代码: <div id="app"> <ul> <li v-for="item in items" track-by="$index">{{$key}}: {{item}}</li> </ul> </div> <script> var test = {name: "test"}; var vm = new Vue({ el: '#app', data: { items: { name: "wd", age: "27", sex: "man" } } }) </script> 显示内容为: name: wd age: 27 sex: man 这个key也可以使用别名,方法很简单,标签如下写: <li v-for="(a_key,item) in items" track-by="$index">{{a_key}}: {{item}}</li> 这里的a_key就相当于$key 且他们之间不会互相冲突,并能同时使用。 注意:别名对数组有效,$key对数组无效 ⑩遍历顺序: 按照Object.keys()的结果遍历 例如: var a = {a: 1, b: 2, c: 3};Object.keys(a) 其返回结果是: ["a", "b", "c"] 然后会按照这个顺序来遍历,但其结果可能会因为javascript引擎的不同而不同(受影响的是对象); ⑪v-for一个数字 可以对一个数字使用v-for,例如: <li v-for="(a_key,item) in 10" track-by="$index">{{a_key}}: {{item}}</li> 【1】这个数字可以是一个浮点数; 【2】从0开始,到小于这个数字的最大整数(例如10那么则到9,10.1则到10); ⑫显示过滤、排序后的结果: 【1】使用计算属性(computed),返回过滤、排序后的结果; 优点:可自定义,功能更强大,更灵活; 缺点:麻烦; 【2】使用内置过滤器filterBy和orderBy 链接:http://cn.vuejs.org/api/#filterBy 【3】filterBy 简单来说,如果没有被过滤的内容,则被过滤掉, 如果是对象,则对key和val都有效(都会被检索), 如代码: <div id="app"> <input v-model="input"/> <p>请在输入框内输入内容,只会将符合条件的内容显示出来</p> <ul> <li v-for="item in items|filterBy input">{{item}}</li> </ul> </div> <script> var test = {name: "test"}; var vm = new Vue({ el: '#app', data: { input: "", items: { name: "wd", age: "27", sex: "man" } } })</script> 注意: (1)假如输入‘a’,虽然name属性和age属性的值没有a,但是其key有,所以依然会显示; (2)不能只对其值(如果v-for的是一个对象的话)过滤生效,比如 <li v-for="item in items|filterBy input in 'item'">{{item}}</li> 是无效的写法! (3)如果要使用(2)中的方法,必须只能面对对象数组; 如以下: <div id="app"> <input v-model="input"/> <p>请在输入框内输入内容,只会显示name符合的</p> <ul> <li v-for="(key,item) in items|filterBy input in 'name'">age:{{item.age}},name:{{item.name}}</li> </ul> </div> <script> var test = {name: "test"}; var vm = new Vue({ el: '#app', data: { input: "", items: [ {age: 1, name: "abc"}, {age: 2, name: "ab"}, {age: 3, name: "c"} ] } })</script> (4)多字段过滤 如以下: <li v-for="(key,item) in items|filterBy input in 'name''age'">age:{{item.age}},name:{{item.name}}</li> 无论是age符合或者是name符合,都可以正常显示。 (5)动态多字段过滤 <div id="app"> <input v-model="input"/> <p>请在输入框内输入内容,只会显示name符合的</p> <ul> <li v-for="(key,item) in items|filterBy input in List">age:{{item.age}},name:{{item.name}}</li> </ul> <button @click="change">点击搜索age</button> </div> <script> var test = {name: "test"}; var vm = new Vue({ el: '#app', data: { input: "", List: "name", items: [ {age: 1, name: "abc"}, {age: 2, name: "ab"}, {age: 3, name: "c"} ] }, methods: { change: function () { this.List = "age"; } } })</script> (1)初始过滤name,点击按钮过滤age; (2)List可以是字符串,也可以是数组; (3)动态改变依然会生效; 【4】orderBy 用于排序的过滤,可以加参数,参数>=0则为正序排序,<0则为倒序排序 普通写法: <li v-for="(key,item) in items|orderBy 'age'">age:{{item.age}},name:{{item.name}}</li> 根据age值,正序排列,显示: age:2,name:ab age:3,name:c age:4,name:abc <li v-for="(key,item) in items|orderBy 'name' -1"> age:{{item.age}},name:{{item.name}}</li> 根据name,倒序排列(因为参数为-1,其<0),字符串的排列顺序为逐字母比较顺序。 也可以使用函数作为排序条件,具体而言,如代码: <div id="app"> <input v-model="input"/> <p>请在输入框内输入内容,只会显示name符合的</p> <ul> <li v-for="(key,item) in items|orderBy test">age:{{item.age}},name:{{item.name}}</li> </ul> <button @click="test">点击搜索age</button> </div> <script> var test = {name: "test"}; var vm = new Vue({ el: '#app', data: { items: [ {age: 5, name: "abc"}, {age: 2, name: "ab"}, {age: 13, name: "c"}, {age: 33, name: "c"}, {age: 3, name: "c"} ] }, methods: { test: function (a, b) { return a.age - b.age; } } })</script> 他会根据age的值差进行排序,简单来说,a-b则为从小到大(这里指的是被排序的属性)),b-a则为从大到小。 如图结果为: age:2,name:ab age:3,name:c age:5,name:abc age:13,name:c age:33,name:c
先上总结: (十九)标签和API总结(2) vm指new Vue获取的实例 ①当dom标签里的值和data里的值绑定后,更改data对应的值可以实时更新标签里的值; 但后续添加的值是无效的(绑定失败)。 ②将可以将对象直接作为data的一个属性,是有效的(因为对象按值传递); 所以该属性和该对象是全等的; ③vm的接口有: vm.$data是vm的data属性; vm.$el是el属性指向的dom结点; vm.$watch是监视属性变化(比如data里的值)(参照(九)) ④vue实例的声明周期,有几个关键函数: created:事件绑定结束后,函数直接在声明vue实例的时候,作为vue实例中的一个属性,下同。 vm.$mount:挂载dom结点; beforeCompile:加载模板之前; compiled:加载模板之后; ready:完成之后(我猜的); beforeDestroy:摧毁之前; destroyed:摧毁之后; ⑤vm.$mount(挂载的id或者类名) 在new Vue实例的时候,不加el,则表示不挂载只生成,生成之后,可以通过该方法来手动挂载到某个地方,如果符合条件的有多个,则挂载到第一个地方; ⑥v-for遍历数组、对象,可以创建多个标签;比如用于创建表格; ⑦转义:{{}} 两个大括号,不会转义值的html标签; {{{}}} 三个大括号,会将值的html标签转义,即变为html文本; 不能在值内再放入绑定数据(除非使用partials,但我还不会); ⑧在插值的大括号内,可以放入表达式(不能放函数); ⑨在插值的大括号内,加入管道符|,可以使用过滤器; capitalize就是将首字母大写的过滤器; 过滤器只能放在表达式最后,不能成为表达式的一部分; 过滤器可以加参数; 过滤器可以自定义(但目前还不知道自定义的方法); ⑩指令: v-if=”变量名” 当某个值为true时存在; v-bind:属性名=”变量名” 将等号后的变量名(指向vm的data属性里的同名属性),和该标签的html属性绑定在一起。 v-on:事件类型=”函数名” 触发事件类型时,执行methods里的函数; v-on的缩写是@;v-bind的缩写是:(冒号); ⑪计算属性computed 这里的属性,可以当做data属性里的使用;优点是data里的数值变更时,这里会跟着一起改变; 可以使用更复杂的表达式(插值里只能使用简单的表达式); ⑫计算属性的setter和getter 默认是getter(对象的get属性),即当某个值改变时,触发回调函数(或get方法); 当计算属性改变时,需要改变某些值(比如改变10个值,在其他地方写监听这个值就不好),那么则需要设置setter(对象的set属性),即当计算属性改变时,触发set方法; ⑬监视属性vm.$watch(被监视的属性, 回调函数) 监视的是data属性; 回调函数的第一个参数是改变后的值,第二个参数是改变前的值; 属性的值改变时触发; ⑭class绑定: 用v-bind:class class使用对象形式,key为class类名,值表示是否显示这个class类; 可以直接将一个object对象放置在v-bind:class的值中,并将这个对象放置在data属性中,这样设置这个object对象的属性即可; class的数组写法:数组里的成员为变量名,如果该变量不是object对象,则变量的值为类名;如果是对象时,对象的key是类名,值表示是否显示; ⑮style绑定: 用v-bind:style 形式是一个对象,对象的key是样式名(如fontSize,注意样式名需要采用驼峰式而不是css式),值是样式的值; 可以直接将对象名放在v-bind:style的等式右边; 对象的值改变,将实时影响内联样式; 对于某些样式,可以针对浏览器加前缀(但某些不能对所有浏览器兼容); (十七)计算属性 ①简单来说,假如data里面有属性a=1,然后你需要一个变量跟着a变化,例如b=a+1,那么就需要用到计算属性,Vue实例的computed属性中,设置b为其属性,其表现为一个函数,返回值是b的值。 具体见代码: [html] view plain copy <div id="app"> <table> <tr> <td>a</td> <td>b=a+1</td> </tr> <tr> <td>{{a}}</td> <td>{{b}}</td> </tr> </table> <button @click="add">a = a + 1</button> </div> <script> var vm = new Vue({ el: "#app", data: { a: 1 }, methods: { add: function () { this.a++; } }, computed: { b: function () { return this.a + 1; } } }) </script> 效果: 初始a的值为1,b的值为2。 点击按钮,会让a的值增加1(注意,没有动b的值)。 但由于b和a是相关的(这里的this.a指的是a),因此,在a的值改变后,b的值也会跟着改变。 之所以这么做,回想一下,Vuejs禁止在变量绑定时输入一个函数,因此如果表达式比较复杂,那么就必须这么做,好处是可以防止模板太重(放很大的表达式在模板中)。 ②vm.$watch(“属性”, function(newVal, oldVal){ //回调函数的具体内容 }) 用于观察Vue实例上的数据变动; 【1】可以监视data属性中的某个属性(比如上面的a); 【2】可以监视computed属性中,某个属性的值,例如上面的b;支持字符串的变化,如代码: [javascript] view plain copy var vm = new Vue({ el: "#app", data: { a: 1 }, methods: { add: function () { this.a++; } }, computed: { b: function () { var str = ""; for (var i = 0; i < this.a; i++) { str += String(i); } return str; } } }) vm.$watch("b", function (val) { alert(val); }) 这里的监视b是有效的。 但假如b返回的是一个固定的字符串,或者值,那么则不会触发(因为值没有改变) 【3】另外,在$watch的回调函数中,第一个参数val的值是新值(即变动后的值),他也可以有第二个参数,而第二个参数的值是旧值(即变动前的值)。 【4】watch的回调函数里,this指向的是vm这个对象; ③setter 计算属性默认是getter(写作get),可以这么理解,他监视某个值,那个值变化时会触发这个回调函数; 但也可以设置为setter(写作set),setter和getter的区别在于,setter是当computed这个属性的值变化时所触发的。例如: [javascript] view plain copy <div id="app"> <input v-model="firstName"/> <input v-model="lastName"/> <input v-model="fullName"/> </div> <script> var vm = new Vue({ el: '#app', data: { firstName: 'Foo', lastName: 'Bar' }, computed: { fullName: { // getter get: function () { return this.firstName + ' ' + this.lastName }, // setter set: function (newValue) { var names = newValue.split(' ') this.firstName = names[0] this.lastName = names[names.length - 1] } } } }) </script> 我们修改前两个输入框的值,将影响第三个输入框的值; 我们也可以修改第三个输入框的值,来影响前两个输入框的值。 另外,由于这种绑定形式,我们将无法让fullName的名字是三个单词,原因在于,set触发了lastName的改变(获取最后一个单词),而lastName的改变又会触发getter(将firstName和lastName拼接起来),因此只会保留第一个单词和最后一个单词。 (十八)Class和Style绑定 ①简单来说,就是用一个变量来控制某个class是否存在与dom之中,这样不需要直接操纵dom的class属性。 如果不想要他影响某个属性,那么就将他放在class里面,而不是绑定的class里面。 具体方法如下,将以下内容放到html标签里: [html] view plain copy v-bind:class="{'green':a,'red':b}" 效果是,假如变量a的值是true(或者可以被隐式转换为true),那么class属性里则添加green,如果b为true,那么red也会被添加。注意,这二者不是互斥的。 如代码: [html] view plain copy <style> .green { background-color: green; } .red { background-color: red; } </style> <div id="app"> <div v-bind:class="{'green':a,'red':b}">背景颜色</div> <button @click="change">变色</button> </div> <script> var vm = new Vue({ el: '#app', data: { a: true, b: false }, methods: { change: function () { this.a = !this.a; this.b = !this.b; } } }) </script> 效果: 点击按钮可以变换a和b的值,从而可以带动class的变化,于是背景颜色也会变。 ②另外一种绑定方法,将class的值放置在data里。 方式: [javascript] view plain copy <div v-bind:class="itsClass">背景颜色</div> //略 data: { itsClass: { green: true } }, 即将变量名放在指令里,然后通过修改属性的值来控制class 优点: 如果需要添加一个class时,只需要在变量里添加属性,并设置该属性为true即可。相对来说更加自由。 如代码: [html] view plain copy <div id="app"> <div v-bind:class="itsClass">背景颜色</div> <button @click="change">变色</button> </div> <script> var vm = new Vue({ el: '#app', data: { itsClass: { green: true } }, methods: { change: function () { this.itsClass.green = false; this.itsClass.red = true; } } }) </script> 点击按钮,虽然原本itsClass这个变量并没有red这个属性,但后续添加这个属性也会在该div里添加red这个class类名。所以,背景颜色从绿色变为红色。 进阶使用: 假如需要当一个值存在时,拥有特殊的样式,那么这个值的变量名可以和样式名一致,并通过这样的方式,当该值存在时,其被隐式转换为true,因此样式为我们需要的样式。 ③class的数组语法和表达式: 写法: [javascript] view plain copy <div v-bind:class="[g,r?r:s]">颜色</div> 效果: g、r、s都是变量名,使用的时候取该变量的值; 如代码: [html] view plain copy <style> .fontGreen { color: green; } .backRed { background-color: red; } .fontSize { font-size: 50px; } </style> <div id="app"> <div v-bind:class="[g,r?r:s]">颜色</div> <button @click="change">变换</button> </div> <script> var vm = new Vue({ el: '#app', data: { g: "fontGreen", r: "backRed", s: "fontSize" }, methods: { change: function () { this.r = false; } } }) </script> 首先他是数组,因此第一个g存在,因此他有样式名fontGreen,其次,是一个三元表达式,他会判断r的值,如果为true(或隐式转换为true),那么就绘将r的值添加到数组,否则添加s的值。 因此,初始表现是绿色红色背景;点击按钮后,显示的是绿色的大字体。 ④在1.0.19+版本里,可以在数组语法中使用对象。 数组格式如下: [html] view plain copy <div v-bind:class="[classG, {R:classR,S:classS}]">颜色</div> data属性如下: [javascript] view plain copy data: { classG: "G", classR: true, classS: true }, 解释: 【1】 数组形式(如classG),需要放置的是变量名,类名是变量名的值; 【2】 对象形式(如{R:classR,S:classS}),对象的key是类名,value用于控制该类是否存在(true或者false); ⑤绑定内联样式: 格式: [html] view plain copy <div v-bind:style="{fontSize:TheSize}">内联样式</div> 【1】 首先,其内是一个对象,key为样式名(如fontSize),value为样式的值(如TheSize,他是data里面的一个变量); 【2】 其不是css,因此不能像css那样写,事实上是一个js的对象,需要采用驼峰写法(教程说可以用短横分隔符命名,但我失败了,例如font-size是不行的); 【3】 可以直接将变量(如果是一个有效对象)放置在这里,例如: [html] view plain copy <div id="app"> <div v-bind:style="TheStyle">内联样式</div> </div> <script> var vm = new Vue({ el: '#app', data: { TheStyle: { fontSize: "30px", color: "red", backgroundColor: "#aaa" } } }) </script> 渲染结果是: [html] view plain copy <div style="font-size: 30px; color: red; background-color: rgb(170, 170, 170);">内联样式</div> 【4】 另外,假如绑定的对象的值被更改,那么内联样式也会被实时更改。 ⑥内联样式的数组写法: 非常简单,使用数组,然后把对象放在其中即可。 例如: [html] view plain copy <div v-bind:style="[TheStyle, AnotherStyle]">内联样式</div> //略 data: { TheStyle: { fontSize: "30px" }, AnotherStyle: { color: "red", backgroundColor: "#aaa" } }, 该div会自动合并两个对象的值并添加到标签中。 唯一需要注意的是,假如有一个对象的值是无效的,那么这个标签的其他对象的值也无法作用到标签上。 ⑦内联样式的自动添加浏览器前缀适应: 最简单的例子,滤镜功能在chrome下是必须添加-webkit-前缀的,否则无效,但IE下无需添加。 [html] view plain copy <div v-bind:style="TheStyle">内联样式</div> //略 TheStyle: { filter: "grayscale(1)" } 在chrome浏览器下,变为: -webkit-filter: grayscale(1); 在Edge浏览器下变为: filter: grayscale(1) 但是在IE11和firefox48的情况下,他是不能正常工作的。 所以个人觉得还是不要指望他了吧。
原教程: http://cn.vuejs.org/guide/instance.html http://cn.vuejs.org/guide/syntax.html 本博文是在原教程的基础上加上实例,并尝试说明的更详细。 (十)Vue实例的生命周期 如图:(我自己翻译的中文版,英文版请查看本博文顶部的,第一个链接) (八)传入的数据绑定 先创建一个对象(假如是obj),然后将他传入Vue实例中,作为data属性的值,那么 ①obj的值的变化,将影响Vue实例中的值的变化; ②相反一样; ③可以在Vue实例外面操纵obj,一样对Vue实例有影响; ④获取obj.a的值(假如他有这个属性),可以通过Vue实例(例如变量vm),vm.a这样的形式来获取(他们是等价的,也是绑定的); ⑤后续添加的数值是无效的 例如: [javascript] view plain copy <div id="app"> {{a}} </div> <button onclick="add()">+1</button> <script> var obj = {a: 1} var vm = new Vue({ el: '#app', data: obj }) function add() { //vm.a++; obj.a++; } </script> add函数中两条语句效果是等价的,都可以让显示的值+1 但若将代码改成这样: [javascript] view plain copy var obj = {b: 1} var vm = new Vue({ el: '#app', data: obj }) function add() { obj.a = 1; } 那么在点击按钮后,并不会显示值(没有绑定)。 注意:即使修改为vm.a=1也是无效的 准确的说,在Vue实例创建后,添加新的属性到实例上,是不会触发视图更新的。 在以上情况下,obj.a === vm.a ,注意,a之前没有data。 函数: [javascript] view plain copy function test() { if (vm.a === obj.a) { console.log("vm.a === obj.a"); } } 其判断条件是true (九)Vue实例暴露的接口 在上一篇中,提到vm.a=obj.a这个;然而我们并没有获取全部的data这个属性; 而Vue提供了几个暴露的接口: 接口(假设实例为vm) 效果 vm.$data 是vm的data属性 vm.$el 是vm的el属性所指向的dom结点 vm.$watch 示例: vm.$watch(“a”,function(newVal, oldVal){}) 当data里的a变化时,会触发回调函数 更多的可以查看 http://cn.vuejs.org/api/ 搜索 $ 作为关键词来查看。 (十一)$mount()手动挂载 当Vue实例没有el属性时,则该实例尚没有挂载到某个dom中; 假如需要延迟挂载,可以在之后手动调用vm.$mount()方法来挂载。例如: [javascript] view plain copy <div id="app"> {{a}} </div> <button onclick="test()">挂载</button> <script> var obj = {a: 1} var vm = new Vue({ data: obj }) function test() { vm.$mount("#app"); } 初始,显示的是{{a}} 当点击按钮后,变成了1 (十二)用Vue的v-for写一个表格 [javascript] view plain copy <!DOCTYPE html> <html> <head> <title>Vue</title> <script src="vue.js"></script> </head> <body> <div id="app"> <button onclick="load()">点击挂载表格</button> </div> <style> table { border-collapse: collapse; border-spacing: 0; border-left: 1px solid #888; border-top: 1px solid #888; background: #efefef; } th, td { border-right: 1px solid #888; border-bottom: 1px solid #888; padding: 5px 15px; } th { font-weight: bold; background: #ccc; } </style> <script> var obj = { grid: [ {id: "ID", name: "名字", description: "描述", clickButton: "点击事件"}, {id: "1", name: "a", description: "amorous", clickButton: "点击弹窗"}, {id: "2", name: "b", description: "beautiful", clickButton: "点击弹窗"}, {id: "3", name: "c", description: "clever", clickButton: "点击弹窗"}, {id: "4", name: "d", description: "delicious", clickButton: "点击弹窗"}, ] } var vm = new Vue({ data: obj, template: '<table><tr v-for="row in grid">' + '<td>{{row.id}}</td>' + '<td>{{row.name}}</td>' + '<td>{{row.description}}</td>' + '<td><button v-on:click="alert($index)">{{row.clickButton}}</button></td>' + '</tr></table>', methods: { alert: function (index) { alert("该行是第" + index + "行") } } }) function load() { vm.$mount("#app"); } </script> </body> </html> (十三)数据绑定: html标签的纯文本显示/被当做html标签处理; 插值单次更新; ①使用两个大括号时,假如字符串内容是html标签,那么不会被转义,而是正常显示; ②使用三个打括号时,字符串内的html标签会被直接转义, 例如: [javascript] view plain copy <div id="app"> {{html}} </div> <script> var vm = new Vue({ el:"#app", data: { html:"<span>span</span>" } }) </script> 屏幕上显示内容是: [javascript] view plain copy <span>span</span> 如果是三个大括号包含变量名: [javascript] view plain copy <div id="app"> {{{html}}} </div> <script> var vm = new Vue({ el:"#app", data: { html:"<span>span</span>" } }) </script> <script> function load() { vm.$mount("#app"); } </script> 显示的内容则只有 span ③插入内容的数据绑定无效(在没有使用partials的情况下) 使用两个大括号或者三个大括号都一样 例如,将②中的html改为以下值 [javascript] view plain copy data: { html: "<span>span{{val}}</span>", val: "11" } 显示结果乃是: span{{val}} 说明没有绑定数据; 按照说明,使用partials可以绑定 http://cn.vuejs.org/api/#partial 不过不会用,等研究明白了再说 ④禁止在用户提交的内容上动态渲染,否则会受到XSS攻击 ⑤插值也可以用在html标签的属性中,例如class,或者id,或者其他。 但是Vue.js的指令和特殊特性内是不可以用插值的。 (十四)绑定表达式 插值的位置,可以使用JavaScript的表达式,例如: [javascript] view plain copy <div id="app"> {{html?html:val}} </div> <script> var vm = new Vue({ el: "#app", data: { html: "", val: "11" } }) </script> 例如以上示例, 假如有html值,则输出hmtl值,否则输出val值; 也可以输出字符串,例如改为: [javascript] view plain copy {{html?html:"no words"}} 则输出no words 但是只能输出表达式,不能输出比如函数,或者直接放个v-for标签之类的。 但是我推断后者应该可以,可能是我写的方法不对。 (十五)过滤器 ①简单来说,在插值中,加入管道符“|”,然后过滤器会生效。 例如: capitalize这个过滤器,会将字符串的首字母大写 [javascript] view plain copy <div id="app"> {{{html|capitalize}}} </div> <script> var vm = new Vue({ el: "#app", data: { html: "abc", val: "11" } }) </script> 输出值是Abc 如果是汉字、数字、或者是本身首字母就大写了,则无反应。 ②过滤器不能充当表达式使用,因此不能在表达式内使用过滤器,只能在表达式的后面使用。 例如: [javascript] view plain copy {{html[0]|capitalize}} 是可以的,会输出html的首个字母并将其大写; 但 [javascript] view plain copy (html|capitalize)[0] 是会报错的(不加括号也报错),说明,不能将过滤器视为表达式的一部分 ③过滤器可以加参数。 第一个参数:固定为表达式的值(被过滤目标); 第二个参数,过滤器后面的第一个单词; 第三个参数,过滤器后面的第二个单词,依次类推。 参数加引号则视为字符串,参数不加引号则视为表达式,表达式的值作为参数传递给过滤器。 官方例子是: {{ message | filterA 'arg1' arg2 }} ④过滤器可以自己手写 (十六)指令 ①指令(Directives)就是特殊的,以带有前缀v-的特性。 简单粗暴来说,标签里v-开头的就是指令(当然,要Vue能支持)。 指令的值限定为 绑定表达式,就是等号后引号里的。 如: [javascript] view plain copy <div id="app"> <div v-if="html"> {{val}} </div> <button onclick="test()">消失上一行</button> </div> <script> var vm = new Vue({ el: "#app", data: { html: "abc", val: "11" } }) function test() { vm.html = ""; } </script> 输出11 其中<div v-if=”html”>就是指令, 可以通过点击按钮让那一行消失(因为html的值被设置为空) ②指令后面可以添加参数: 有些指令(例如v-bind)可以在名称后等号前,添加一个属性,这个属性的作用是响应性的更新HTML特性。 例如: [javascript] view plain copy <style> .white { background-color: white; } .black { background-color: black; } </style> <div id="app"> <div v-bind:class="BC">背景颜色变化</div> <button onclick="test()">消失上一行</button> </div> <script> var vm = new Vue({ el: "#app", data: { BC: "black" } }) function test() { vm.BC = "white"; } </script> 初始情况下,这个div的class和data里的BC绑定,由于BC的值是black,那么相当于v-bind所在的标签的class=”black”,所以初始情况下,背景颜色为黑色。 当点击按钮后,BC的值被更改为white,那么相当于标签的class=”white”,而类white的背景颜色为白色,所以该div的背景颜色变成了白色。 类似的有v-on:click事件,表示监视的是click事件,也可以改为 [javascript] view plain copy <div v-on:mouseup="alert">背景颜色变化</div> 表示该标签当鼠标弹起的时候,执行methods的alert函数。 ③修饰符 修饰符用于表示指令应当以特殊的方式进行绑定。 例如:.literal修饰符告诉指令应当将他的值解析为字符串,而不是表达式 或者是keydown.enter表示按回车键时调用函数 [javascript] view plain copy <input v-on:keydown.enter="alert"></input> ④缩写: v-on的缩写是@ shift+数字2 v-bind的缩写是:就是冒号
参照链接: http://cn.vuejs.org/guide/index.html 【起步】部分 本文是在其基础上进行补全和更详细的探寻 嗯,根据朋友的建议,我改投vue阵营了 (一)单向绑定 [javascript] view plain copy <div id="app"> {{ message }} </div> <script> new Vue({ el: '#app', data: { message: 'Hello Vue.js!' } }) </script> ①el应该表示绑定的意思,绑定id=app这个标签 也可以改为以下这样: [javascript] view plain copy <div class="app"> {{ message }} </div> [javascript] view plain copy el: '.app', 一样有效。 但如果是多个的话,只对第一个有效: [html] view plain copy <div class="app"> {{ message }} </div> <div class="app"> {{ message }} </div> Hello Vue.js! {{ message }} ②data里的message变量,表示{{message}的值 (二)双向绑定 [javascript] view plain copy <div id="app"> {{ message }} <br/> <input v-model="message"/> </div> <script> new Vue({ el: '#app', data: { message: 'Hello Vue.js!' } }) </script> 效果是: ①input输入框里有初始值,值是data里的message属性的值; ②修改输入框的值可以影响外面的值; (三)函数返回值 [javascript] view plain copy <div id="app"> {{ message() }} <br/> <input v-model="message()"/> </div> <script> new Vue({ el: '#app', data: { message: function () { return 'Hello Vue.js!'; } } }) </script> 效果: ①输出值也是message的返回值; ②缺点:失去双向绑定! (四)渲染列表 [javascript] view plain copy <div id="app"> <ul> <li v-for="list in todos"> {{list.text}} </li> </ul> </div> <script> new Vue({ el: '#app', data: { todos: [ {text: "1st"}, {text: "2nd"}, {text: "3rd"} ] } }) </script> v-for里的list,类似for in里面的i, 个人认为, ①可以把list in todos,理解为for list in todos ②然后把下一行的list.text理解为 todos[list].text 然后这个v-for标签在哪里,就是以他为单位进行多次复制。 (五)处理用户输入 [javascript] view plain copy <div id="app"> <input v-model="message"> <input type="button" value="值+1" v-on:click="add"/> <input type="button" value="值-1" v-on:click="minus"/> <input type="button" value="重置归零" v-on:click="reset"/> </div> <script> new Vue({ el: '#app', data: { message: 1 }, methods: { add: function () { this.message++; //这步要加this才能正确获取到值 }, minus: function () { this.message--; }, reset: function () { this.message = 0; } } }) </script> 效果: ①对输入框的值,点击一次add按钮,则值+1; ②如果不能加,则像正常表达式加错了那样返回结果,例如NaN; ③data里的message的值,是初始值; ④methods里是函数集合,他们之间用逗号分隔; ⑤获取值的时候,要加上this,例如this.message获取的是message的值。 (六)多功能 [javascript] view plain copy <div id="app"> <input v-model="val" v-on:keypress.enter="addToList"> <ul> <li v-for="val in values"> {{val.val}} <input type="button" value="删除" v-on:click="removeList($index)"/> </li> </ul> </div> <script> new Vue({ el: '#app', data: { val: "1", values: [] }, methods: { addToList: function () { var val = parseInt(this.val.trim()); //注意,因为当上面的val是字符串类型的时候,才能用trim(),如果是数字类型,则用this.val if (val) { this.values.push({val: val}); } this.val = String(val + 1); }, removeList: function (index) { this.values.splice(index, 1); } } }) </script> 效果: ①初始输入框内值为1; ②在输入框内按回车键,则会将输入框的内容转为数字,并添加到一个列表里,该列表里转换后的数字和一个删除按钮,并且输入框内的值,变为转为数字后的值加一。 如图: ③他的添加,利用的是双向绑定,将输入的值push到data里面的values这个数组之种,然后利用渲染列表的效果,输出多行值。 ④在button标签里,函数的参数名给了一个参数,是该行索引,参数名是$index ⑤标签里,触发的函数的函数名,可以加括号,也可以不加括号,实测似乎是没有影响的。 (七)标签和API总结(1) ① {{ 变量名 }} 表示绑定的变量,调用时需要用this.变量名 ② v-model=”变量” 双向绑定使用,如果input里不加任何type就是文本,如果加type就是type,例如: [javascript] view plain copy <input v-model="DATE" type="date"/> <li>{{DATE}}</li> 就会将日期类型的输入框的值,和li标签显示的内容绑定在一起。 ③ v-on:click=”函数名” 点击时触发该函数,可加()也可以不加, $index作为参数表示索引,索引值从0开始。 ④ v-for 双向绑定的在数组内容更新后,会实时更新,v-model也是; 类似for in语句,被多次使用的是 ⑤ v-on:事件 即触发的事件,有click(点击),keypress(按键按下) 事件后面可以跟更具体的,例如keypress.enter是回车,keypress.space是空格等 更多的需要之查看 ⑥ new vue 通过new一个vue的实例,然后传一个对象作为参数给这个实例; 其中: el 表示绑定的模板(只会匹配到绑定的第一个) data 表示数据,可以直接被取用,例如用在v-model或者是{{变量名}}中 methods 表示方法 ⑦ 函数内部调用变量 通过this.变量名,例如: [javascript] view plain copy data: { val: "1", values: [] }, methods: { addToList: function () { console.log(this.val); 这里的this.val就是上面的data.val,也是html里的{{val}},也是v-model=”val”,但不是 [javascript] view plain copy <li v-for="val in values"> {{val.val}} <input type="button" value="删除" v-on:click="removeList($index)"/> </li> 里面的val.val,至于原因,个人认为是这里的val处于v-for的作用域内,因此val in values 里的val其在作用域链中的优先级更高
DEMO网址: http://jianwangsan.cn/toolbox (五)添加、点击和移动的逻辑 我反思了一下,在(四)中我写的并不好,事实上,无论是大按钮,还是被添加到我的工具,或者是添加到常用工具栏,他都是一个按钮,因此,应该共享状态,即他们属于同一个tool实例,并能互相影响。 需求分析: 在重写Tool类之前,需要明确分析按钮的逻辑。 在全部工具页面: ①当按钮未被添加时,鼠标移动上去会有添加按钮显示; ②当按钮未被添加时,鼠标无论点击按钮本身还是点击添加按钮,都执行添加逻辑,将添加按钮显示为取消,执行一次添加动画,添加完成后,按钮隐藏; ③当按钮已被添加时,鼠标点击会启用按钮本身的逻辑(比如打开软件); 在我的工具页面: ①当按钮未被添加时,不显示; ②当按钮被添加时,显示按钮; ③点击按钮时,点击会启用按钮本身的逻辑(比如打开软件); ④长按按钮,可以拖动按钮,原有按钮位置不被占用,显示为空白; ⑤按钮拖动中时,若经过某个已有按钮的位置,原按钮位置不再被占用,经过的位置被空白占用,相当于把空白占位符从DOM中的原位置挪到了DOM树中的新位置; ⑥按钮拖动时,可以离开工具箱的页面显示范围,但不会显示出来(类似overflow:hidden)的效果; ⑦当按钮拖动到主界面快捷入口的四个图标范围时,若原位置有图标,再该位置图标及右侧所有图标依次向右移动一位;当离开这个位置时,原有图标立刻返回原位置; ⑧主界面快捷入口最多有四个图标,假如新插入一个,那么原本最右边的将被移除; ⑨点击编辑按钮时,所有图标右上角都会出现一个红叉符号; (10)当编辑状态时,点击主界面快捷入口的四个图标的红叉时,将移除被点击的那个图标; (11)当编辑状态时,点击我的工具的图标的红叉,将删除该图标(全部工具里的添加按钮恢复);若该图标在快捷入口也存在时,快捷入口的该图标也被删除。 因此,这个Tool类也能满足以上功能; 更新模板和样式; ①要有四种状态的图标,依次为全部工具页面的大图标、普通图标,我的工具页面的普通图标、快捷入口图标; ②并且,要能创建这些图标,并且,由于之前没有设计全部工具页面的取消按钮、我的工具页面里的编辑按钮,因此要添上; 我的工具里图标的html模板:,编辑按钮默认为不显示: [javascript] view plain copy div.tool-my div.img div.text 小清新日历 div.edit-img.displayNONE 快捷入口的按钮的html模板: [javascript] view plain copy div.tool-foot div.img div.text 系统急救箱 div.edit-img.displayNONE 取消按钮添加后的大图标: [javascript] view plain copy div.BigTool span.img span.mask div.text div.title div.description div.Button.add 添 加 div.Button.cancel.displayNONE 取 消 取消按钮添加后的html模板: [javascript] view plain copy div.normalTool div.img div.text div.title div.description div.Button.add 添 加 div.Button.cancel.displayNONE 取 消 另附2个新增的CSS样式: [javascript] view plain copy .back .contentbox .toolbox-my .toolbox-content .tool-my .edit-img { position: absolute; right: 14px; top: 13px; width: 18px; height: 17px; background-image: url(../img/toolbox.png); background-position: -80px -50px; } .back .contentbox .toolbox-my .toolbox-foot .edit-img { position: absolute; right: 14px; top: 10px; width: 18px; height: 17px; background-image: url(../img/toolbox.png); background-position: -80px -50px; } 然后是样式修改: 显示大图标的: 把原来的addButton拆分成Button、add和cancel [javascript] view plain copy .back .contentbox .toolbox-all .firstRow .BigTool .Button { display: none; position: absolute; bottom: 10px; right: 12px; width: 60px; height: 22px; font-size: 12px; text-align: center; line-height: 20px; -webkit-border-radius: 1px; -moz-border-radius: 1px; border-radius: 1px; } .back .contentbox .toolbox-all .firstRow .BigTool .Button.add { background-image: linear-gradient(rgb(98, 227, 25) 0%, rgb(68, 208, 27) 100%); color: white; border: 1px solid rgb(65, 199, 36); } .back .contentbox .toolbox-all .firstRow .BigTool .Button.cancel { background-image: linear-gradient(#f3f3f3 0%, #dfdfdf 100%); color: black; border: 1px solid #b6b6b6; display: block; } .back .contentbox .toolbox-all .firstRow .BigTool:hover .add { display: block; } 另外一个类似 [javascript] view plain copy .back .contentbox .commonRow .normalTool .Button { display: none; position: absolute; top: 7px; right: 15px; width: 60px; height: 22px; font-size: 12px; text-align: center; line-height: 20px; -webkit-border-radius: 1px; -moz-border-radius: 1px; border-radius: 1px; } .back .contentbox .commonRow .normalTool .add { background-image: linear-gradient(rgb(98, 227, 25) 0%, rgb(68, 208, 27) 100%); border: 1px solid rgb(65, 199, 36); color: white; } .back .contentbox .commonRow .normalTool .cancel { background-image: linear-gradient(#f3f3f3 0%, #dfdfdf 100%); color: black; border: 1px solid #b6b6b6; display: block; } .back .contentbox .commonRow .normalTool:hover .add { display: block; } 为了符合实际需求,我们需要做以下工作: ①重构Tool类(之前实在太简陋了); ②需要一个函数,专用进行操作; ③需要一个runAfter函数,他的效果是,监听某个对象的某个方法,在该方法执行完后执行runAfter的回调函数; ④然后利用这个runAfter监听一些函数,并在该函数触发时做一些事情。 重构Tool类: 他分为以下几个部分: ①变量声明(私有变量): [javascript] view plain copy var Tool = function (obj, myToolDom) { var self = this; var obj = obj; // 0表示未加载到我的工具,1表示加载到我的工具,-1表示添加中(未使用) var state = 0; var BigImgDom = null; //全部工具大图标 var NormalToolDom = null; //全部工具小图标 var addButton = null; //确认按钮 var cancelButton = null; //取消按钮(有,但是实际未使用) var InMyToolDom = null; //我的工具大图标 var editButtonInMyToolDom = null; //我的工具页面的编辑红叉按钮 var SmallToolDom = null; //快捷入口图标 var editButtonInSmallToolDom = null; //快捷入口图标的编辑按钮 var DomClickEvent = null; //这个是该dom点击事件的回调函数 var editing = false; //编辑状态 var move = false; //我的工具大图标移动状态 var SmallIconMove = false; //我的工具小图标移动状态 //如果有传参则用传参,如果没有则用默认值 var MyToolDom = myToolDom ? myToolDom : $(".toolbox-my .toolbox-content"); 注意,以上四种图标,最多只会存在三种(所有工具的大图标和小图标只能存在一种状态) ②dom获取 根据实际需求,我们有时候需要获取dom,其中一个是私有的(只在tool类里调用),一个是公有的(会在外面调用),所以声明方式不同: [javascript] view plain copy //获取DOM var getDomInAllTools = function () { return BigImgDom ? BigImgDom : NormalToolDom; //因为只有一个存在 }; this.getDomInMyTools = function () { return InMyToolDom; }; ③创建图标 我们自然需要创建图标,四种图标都需要一个函数(因为他们的dom结构不同); [javascript] view plain copy //全部工具页面 // 大按钮 this.createBigImgDom = function (callback) { var str = '<div class="BigTool">' + '<span class="img" style="background-position: ' + obj.bigImg.ImgPosition.x + ' ' + obj.bigImg.ImgPosition.y + '"></span>' + '<span class="mask"></span>' + '<div class="text">' + '<div class="title">' + obj.title + '</div>' + '<div class="description">' + obj.description + '</div>' + '</div>' + '<div class="Button add">添 加</div>' + '<div class="Button cancel displayNONE">取 消</div>' + '</div>'; BigImgDom = $(str); addButton = BigImgDom.find(".add"); cancelButton = BigImgDom.find(".cancel"); DomClickEvent = callback; setClickEvent(); return getDomInAllTools(); }; // 普通按钮 this.createNormalToolDom = function (callback) { var str = '<div class="normalTool">' + '<div class="img" style="background-position: ' + obj.commonImg.ImgPosition.x + ' ' + obj.commonImg.ImgPosition.y + '"></div>' + '<div class="text">' + '<div class="title">' + obj.title + '</div>' + '<div class="description">' + obj.description + '</div>' + '</div>' + '<div class="Button add">添 加</div>' + '<div class="Button cancel displayNONE">取 消</div>' + '</div>'; NormalToolDom = $(str); addButton = NormalToolDom.find(".add"); cancelButton = NormalToolDom.find(".cancel"); DomClickEvent = callback; setClickEvent(); return getDomInAllTools(); }; //我的工具页面 // 创建普通的dom var createInMyToolDom = function () { var str = '<div class="tool-my">' + '<div class="img" style="background-position: ' + obj.commonImg.ImgPosition.x + ' ' + obj.commonImg.ImgPosition.y + '"></div>' + '<div class="text">' + obj.title + '</div>' + '<div class="edit-img displayNONE"></div>' + '</div>' var node = $(str); return node; } // 创建小的dom var createSmallTool = function () { var position_x = parseInt(obj.commonImg.ImgPosition.x) * 0.615 + "px"; var position_y = parseInt(obj.commonImg.ImgPosition.y) * 0.615 + "px"; var str = '<div class="tool-foot">' + '<div class="img" style="background-position: ' + position_x + ' ' + position_y + '"></div>' + '<div class="text">' + obj.title + '</div>' + '<div class="edit-img displayNONE"></div>' + '</div>'; var node = $(str); return node; }; 注: (1)以上四个创建图标,在全部工具里的两种还额外获取了编辑按钮; (2)创建全部工具的图标时,顺便获取了响应函数 ④响应逻辑: 图标创建了必然需要对她设置事件, (1)比如全部工具里图标的点击事件: [javascript] view plain copy // 设置全部工具里的点击事件 var setClickEvent = function () { var node = BigImgDom ? BigImgDom : NormalToolDom; node.click(function (event) { if (state) { DomClickEvent(); } else if (state === 0) { self.addDomToMyTools(); } }); }; (2)以上点击事件又分为两种,已添加时,触发正常的响应逻辑(在创建图标时作为参数传进来的回调函数); 未添加时,用于添加到我的工具里的函数: [javascript] view plain copy //将dom添加到我的工具里 this.addDomToMyTools = function () { addButton.addClass("displayNONE"); state = 1; //设置其状态为已添加 //现在是在mytools里添加 if (!InMyToolDom) { InMyToolDom = createInMyToolDom(); editButtonInMyToolDom = InMyToolDom.find(".edit-img"); //setMyToolEditEvent(editButtonInMyToolDom); //废弃,同下被整合 //setMyToolClickEvent(InMyToolDom); //废弃,被整合进移动按钮的逻辑中 setMyToolsDomMoveEvent(); } MyToolDom.append(InMyToolDom); }; (3)这个添加事件又分为: 设置添加按钮为隐藏,设置添加状态为已添加; 没有dom的时候,创建dom并为其绑定事件,绑定的事件有: 【1】点击事件: (但事实上这个已废弃,因为后面涉及到移动按钮的函数,被整合在那里了) [javascript] view plain copy // 设置我的工具页面的两种按钮的点击事件 var setMyToolClickEvent = function (node) { node.click(function (event) { if (!editing) { DomClickEvent(); } }); }; 【2】编辑按钮点击事件: (同样已废弃,因为后面涉及到移动按钮的函数,被整合在那里了) 【3】设置按钮的移动函数,包括以上两个被整合的事件: [javascript] view plain copy //我的工具大图标的移动函数 var setMyToolsDomMoveEvent = function () { var mouseX = null; var mouseY = null; var startLeft = null; var startTop = null; var placeHholderDom = $('<div class="tool-my .ItsplaceHolder"></div>'); InMyToolDom.mousedown(function (evt) { if (evt.button != 0) { return; } if (editing & $(evt.target)[0] == editButtonInMyToolDom[0]) { self.setStateToUnadd(); InMyToolDom.detach(); if ($(".shortcut .tool-foot").filter(placeHholderDom[0])) { SmallToolDom.detach(); $(".shortcut").append('<div class="tool-foot placeholder">' + '<div class="placeholder-img"></div>' + '<div class="text">拖拽到此</div>' + '</div>') } event.stopPropagation(); return; } mouseX = evt.clientX; //这里的值是鼠标坐标 mouseY = evt.clientY; var position = InMyToolDom.position(); startLeft = position.left; //没有px startTop = position.top; InMyToolDom.css("position", "absolute"); InMyToolDom.css("left", startLeft + "px"); InMyToolDom.css("top", startTop + "px"); InMyToolDom.after(placeHholderDom); move = true; }); $(".toolbox-my").mousemove(function (evt) { if (!evt.buttons & move) { //只有不在按,且当前是true的时候,才触发 move = false; placeHholderDom.after(InMyToolDom); placeHholderDom.remove(); InMyToolDom.css("position", "relative"); InMyToolDom.css("left", "0"); InMyToolDom.css("top", "0"); self.InMyToolDomEndMoving(); if (mouseX == evt.clientX & mouseY == evt.clientY) { if (!editing) { DomClickEvent(); } } } if (move) { self.InMyToolDomMoving([placeHholderDom, evt]); var offsetX = evt.clientX - mouseX; var offsetY = evt.clientY - mouseY; InMyToolDom.css("left", offsetX + startLeft + "px"); InMyToolDom.css("top", offsetY + startTop + "px"); } }); InMyToolDom.mouseup(function (evt) { if (move) { move = false; placeHholderDom.after(InMyToolDom); placeHholderDom.remove(); InMyToolDom.css("position", "relative"); InMyToolDom.css("left", "0"); InMyToolDom.css("top", "0"); self.InMyToolDomEndMoving(); if (mouseX == evt.clientX & mouseY == evt.clientY) { if (!editing) { DomClickEvent(); } } } }) }; 解释: 以上这个函数干了以下事: 《1》只有鼠标左键按下才有效; 《2》编辑状态时,按编辑按钮是有效的;(因此我们需要一个设置编辑状态的函数) 《3》编辑状态时点击编辑按钮,会删除当前按钮,并用一个空白的按钮替代; 《4》这个dom移除,为了不移除他的事件,因此用的是jquery的detach()方法,并阻止该按钮事件的冒泡; 《5》非编辑状态时,可以移动按钮,原理是设置该按钮的定位为绝对定位,并用一个空白按钮放在该按钮的DOM后起到占位作用,并设置该按钮可移动; 《6》由于代码本身写的并不是很好,因此移动状态被取消时,计算移动距离,如果没有位移偏差,且非编辑状态,触发该按钮的点击事件;(如果要写的好的话,该按钮的移动状态设置,应该在移动位置有偏差之后才设置position为绝对定位); 《7》移动时触发一个InMyToolDomMoving方法,这个要被runAfter方法所监视,返回值是空白dom和鼠标移动时触发的event事件。该事件会决定该按钮和其他按钮的互动; 《8》移动结束后,会触发一个InMyToolDomEndMoving方法,该方法同样被监视。并且设置移动结束后的鼠标弹起事件(同移动中的那个)。 《9》移动结束后,把图标放在占位图标之前(注意,该占位图标的位置可能移动),并解除移动状态,取消绝对定位,移除占位图标; 【4】然后是我的工具的大图标的移动中事件: [javascript] view plain copy this.InMyToolDomEndMoving = function () { if (!SmallToolDom) { SmallToolDom = createSmallTool(); //setMyToolClickEvent(SmallToolDom); editButtonInSmallToolDom = SmallToolDom.find(".edit-img"); setSmallEditEvent(editButtonInSmallToolDom); setSmallToolDomMoveEvent(); } return SmallToolDom; }; 他会返回小图标,如果没有小图标则创建,并为他绑定事件。 绑定事件有: 《1》点击事件(整合进移动相关函数); 《2》编辑按钮点击事件: [javascript] view plain copy // 设置小的dom的编辑按钮的点击事件 var setSmallEditEvent = function (editNode) { editNode.click(function (event) { self.removeSmallTool(); editButtonInSmallToolDom.addClass("displayNONE"); SmallToolDom.detach(); event.stopPropagation(); }) }; 《3》移动事件; [javascript] view plain copy var setSmallToolDomMoveEvent = function () { var mouseX = null; var mouseY = null; var startLeft = null; var startTop = null; var placeHholderDom = $('<div class="tool-foot placeholder SmallToolHolder">' + '<div class="placeholder-img"></div>' + '<div class="text">拖拽到此</div>' + '</div>'); SmallToolDom.mousedown(function (evt) { if (evt.button != 0) { return; } if (editing & $(evt.target)[0] == editButtonInSmallToolDom[0]) { event.stopPropagation(); return; } mouseX = evt.clientX; //这里的值是鼠标坐标 mouseY = evt.clientY; var position = SmallToolDom.position(); startLeft = position.left; //没有px startTop = position.top; SmallToolDom.css("position", "absolute"); SmallToolDom.css("left", startLeft + "px"); SmallToolDom.css("top", startTop + "px"); SmallToolDom.after(placeHholderDom); SmallIconMove = true; if ($(".SmallToolHolder").length > 1) { $(".SmallToolHolder")[1].remove(); } }); $(".toolbox-my").mousemove(function (evt) { if (!evt.buttons & SmallIconMove) { //只有不在按,且当前是true的时候,才触发 SmallIconMove = false; placeHholderDom.before(SmallToolDom); placeHholderDom.remove(); SmallToolDom.css("position", "relative"); SmallToolDom.css("left", "0"); SmallToolDom.css("top", "0"); $(".SmallToolHolder").remove(); if (mouseX == evt.clientX & mouseY == evt.clientY) { if (!editing) { DomClickEvent(); } } } if (SmallIconMove) { self.SmallToolDomMoving([placeHholderDom, evt]); var offsetX = evt.clientX - mouseX; var offsetY = evt.clientY - mouseY; SmallToolDom.css("left", offsetX + startLeft + "px"); SmallToolDom.css("top", offsetY + startTop + "px"); if ($(".SmallToolHolder").length > 1) { $(".SmallToolHolder")[1].remove(); } } }); SmallToolDom.mouseup(function (evt) { if (SmallIconMove) { SmallIconMove = false; placeHholderDom.before(SmallToolDom); placeHholderDom.remove(); SmallToolDom.css("position", "relative"); SmallToolDom.css("left", "0"); SmallToolDom.css("top", "0"); $(".SmallToolHolder").remove(); if (mouseX == evt.clientX & mouseY == evt.clientY) { if (!editing) { DomClickEvent(); } } } }) } 由于和大图标的移动按钮事件类似,就不细说了 总之,他在移动的时候,会触发一个SmallToolDomMoving方法,runAfter会监视他。 【5】下来是两个按钮移动时触发的事件和移动结束后触发的事件: [javascript] view plain copy //移动时会触发这个方法 this.InMyToolDomMoving = function (arr) { return arr; }; this.InMyToolDomEndMoving = function () { if (!SmallToolDom) { SmallToolDom = createSmallTool(); //setMyToolClickEvent(SmallToolDom); editButtonInSmallToolDom = SmallToolDom.find(".edit-img"); setSmallEditEvent(editButtonInSmallToolDom); setSmallToolDomMoveEvent(); } return SmallToolDom; }; this.SmallToolDomMoving = function (arr) { return arr; }; ⑤其他事件: [javascript] view plain copy //设置编辑或者结束编辑 this.setEditing = function () { editing = true; editButtonInMyToolDom ? editButtonInMyToolDom.removeClass("displayNONE") : ""; editButtonInSmallToolDom ? editButtonInSmallToolDom.removeClass("displayNONE") : ""; }; this.cancelEditing = function () { editing = false; editButtonInMyToolDom.addClass("displayNONE"); editButtonInSmallToolDom ? editButtonInSmallToolDom.addClass("displayNONE") : ""; } //设置dom为未添加状态 this.setStateToUnadd = function () { addButton.removeClass("displayNONE"); editButtonInMyToolDom.addClass("displayNONE"); if (editButtonInSmallToolDom) { editButtonInSmallToolDom.addClass("displayNONE"); } state = 0; //设置其状态为未添加 } 编辑状态的两个方法用于设置可编辑或者不可编辑; 另一个用于在删除按钮时调用; —————————————————————————— 重写ToolsConfigJsonLoad函数: 这个函数做了这些事情: ①读取json; ②把json转化为Tool类的数据来源; ③创建Tool类实例,并监听其事件; ④在创建图标的时候,添加分割线、或者创建占位图标等; 声明这个函数: [javascript] view plain copy var ToolsConfigJsonLoad = function (InMyToolArray, url) { this.url = url ? url : "data/tools.json"; 这个函数只有一个公有方法,那就是读取json: [javascript] view plain copy this.load = function () { var self = this; $.ajax({ url: self.url, dataType: "json", type: "GET", success: function (data) { addToolsInToolbox_all(data); } }) }; 读取json成功之后调用的函数: [javascript] view plain copy //将内容添加到全部工具页面中 var addToolsInToolbox_all = function (data) { var type = []; data[0].BigImg.forEach(function (obj) { var tool = new Tool(obj); var mixin = new MixinTool(obj); var callback = mixin.mixin() listenToolEvent(tool); $(".firstRow").append(tool.createBigImgDom(callback)); }) data[0].CommonImg.forEach(function (obj) { if (type.indexOf(obj.type) < 0) { type.push(obj.type); } var tool = new Tool(obj); var mixin = new MixinTool(obj); var callback = mixin.mixin() listenToolEvent(tool); $(".commonRow." + obj.type).append(tool.createNormalToolDom(callback)); }) addPlaceHolderWhenOnlyTwoToolsInToolbox_All(type); addDottedLineInToolbox_All(); }; 监听事件先略过; 添加分割线: [javascript] view plain copy // 这个目的是为了给全部工具中的多行工具之间添加分割线 var addDottedLineInToolbox_All = function () { $(".commonRow .normalTool:nth-child(3n+4)").before('<div class="dotted"></div>'); }; 为保证美观,添加占位图标: [javascript] view plain copy // 这个目的是当某一行只有两个图标时,创造一个占位的图标 var addPlaceHolderWhenOnlyTwoToolsInToolbox_All = function (type) { type.forEach(function (obj) { var length = $(".commonRow." + obj + " > *").length; if (length % 3 == 2) { $(".commonRow." + obj).append($('<div class="normalToolHolder" style="cursor:default"></div>')); } }) }; 下来事件监听函数的原型: [javascript] view plain copy //参数1是对象,参数2是方法名(字符串),参数三是该方法执行后执行的函数 var runAfter = function (obj, runEvent, AfterEvent) { var temp = obj[runEvent]; obj[runEvent] = function (arguments) { var result = temp(arguments); AfterEvent(obj, result); } } 类似dojo的aspect.after方法 最后是利用runAfter函数进行事件监听,调用它时只需要传递Tool类的实例。 [javascript] view plain copy //监听事件 var listenToolEvent = function (tool) { 他包含以下方法: ①当图标添加进我的工具时,将实例添加到一个数组中: [javascript] view plain copy runAfter(tool, "addDomToMyTools", function () { InMyToolArray.push(tool); }); ②或者是删除我的工具里的按钮时,移除这个实例: [javascript] view plain copy runAfter(tool, "setStateToUnadd", function () { var MyToolIndex = InMyToolArray.indexOf(tool); InMyToolArray.splice(MyToolIndex, 1); }); [javascript] view plain copy </pre><p>③还需要一个占位图标,用于图标在移动时占位:</p><pre name="code" class="javascript">var placeHolderInSmall = $('<div class="tool-foot placeholder">' + '<div class="placeholder-img"></div>' + '<div class="text">拖拽到此</div>' + '</div>') ④监听图标移动时的方法,他涉及到我的工具页面里大图标的互动,和快捷入口小图标的互动: [javascript] view plain copy runAfter(tool, "InMyToolDomMoving", function (obj, result) { var placeHolder = result[0]; var event = result[1]; InMyToolArray.forEach(function (tool) { var node = tool.getDomInMyTools(); if (node.css("position") !== "absolute") { var position = node.offset(); //获取相对于文档的位置 if (event.clientY > position.top & event.clientY < position.top + 100 & event.clientX > position.left & event.clientX < position.left + 100) { if (node.index() < placeHolder.index()) { //根据索引决定放在前还是后面 node.before(placeHolder); } else { node.after(placeHolder); } } } }); var theNodeInSmallTools = false; //是否重合 $(".tool-foot").toArray().forEach(function (node, index) { var position = $(node).offset(); //获取相对于文档的位置 if (event.clientY > position.top & event.clientY < position.top + 70 & event.clientX > position.left & event.clientX < position.left + 76) { theNodeInSmallTools = true; if ($(node) != placeHolderInSmall) { $(node).before(placeHolderInSmall); } } }); //如果重合 if (theNodeInSmallTools) { $(".shortcut .tool-foot:last-child").addClass("displayNONE"); } //如果不重合 else { placeHolderInSmall.remove(); $(".shortcut .tool-foot.displayNONE").removeClass("displayNONE"); } }); [javascript] view plain copy </pre><p>⑤当图标停止移动时,需要决定他是否被移动到一个新位置,或者是添加到快捷入口那里;</p><pre name="code" class="javascript">runAfter(tool, "InMyToolDomEndMoving", function (obj, node) { var sign = false; $(".tool-foot").toArray().forEach(function (node, index) { if ($(node)[0] == placeHolderInSmall[0]) { sign = true; } }) if (sign) { if (node.hasClass("displayNONE")) { node.removeClass("displayNONE"); } placeHolderInSmall.before(node); placeHolderInSmall.remove(); } if ($(".tool-foot").length < 4) { var temp = '<div class="tool-foot placeholder">' + '<div class="placeholder-img"></div>' + '<div class="text">拖拽到此</div>' + '</div>'; $(".shortcut").append(temp); } else if ($(".tool-foot").length > 4) { $(".tool-foot")[4].remove(); //移除第五个 } else { $(".tool-foot.displayNONE").removeClass("displayNONE"); } $(".shortcut").append($(".tool-foot.placeholder")); }) ⑥还有小图标移动时,需要和小图标互动,例如可能需要交换位置: runAfter(tool, "SmallToolDomMoving", function (obj, result) { var placeHolder = result[0]; var event = result[1]; $(".tool-foot").toArray().forEach(function (node) { var position = $(node).offset(); //获取相对于文档的位置 if ($(node).css("position") !== "absolute") { if (event.clientY > position.top & event.clientY < position.top + 70 & event.clientX > position.left & event.clientX < position.left + 76) { if ($(node).index() < placeHolder.index()) { //根据索引决定放在前还是后面 $(node).before(placeHolder); } else { $(node).after(placeHolder); } } } }); }); —————————————————————— 让工具箱运行起来: 以上代码,只涉及到工具箱页面各个图标的交互逻辑,但是并没有真正运行起来(例如,没有显式调用load()方法),并且也没有编辑图标的逻辑。 因此,我们需要一个函数补全剩下的部分: [javascript] view plain copy var ToolBoxEvent = function () { var InMyToolArray = []; var jsonLoad = new ToolsConfigJsonLoad(InMyToolArray); jsonLoad.load(); $("#edit").click(function () { //编辑中 if ($(this).hasClass("editing")) { InMyToolArray.forEach(function (item) { item.cancelEditing(); }) //设置编辑按钮的样式变更 $(this).removeClass("editing"); var text = $(this).find(".text"); text.text("编辑"); text.css("width", "32px"); text.css("margin-left", "0px"); } else { InMyToolArray.forEach(function (item) { item.setEditing(); }) //设置编辑按钮的样式变更 $(this).addClass("editing"); var text = $(this).find(".text"); text.text("退出编辑"); text.css("width", "52px"); text.css("margin-left", "-10px"); } }); } 这个函数干了两件事: ①声明一个ToolsConfigJsonLoad类的实例,并调用它的load方法(加载工具箱); ②设置编辑按钮的事件。 这样的话,工具箱就可以正常跑起来了。 当然,还有一些小BUG,需要被修复,不过这并不影响整体功能的运转,作为一个DEMO来说,程度是足够了,如果真要跑生产环境,那么这些BUG必须被fix js全部代码: http://jianwangsan.cn/javascripts/toolboxes.js css全部代码: http://jianwangsan.cn/stylesheets/toolboxes.css
DEMO网址: http://jianwangsan.cn/toolbox (四)制作JSON,自动将图标填充进所有工具 首先是JSON,因为工具很多,所以JSON内容很长。 具体而言,JSON是一个数组中的对象(只有这一个对象),他有两个属性:BigImg和CommonImg。 这两个属性都是数组类型; BigImg里面,他用于存放最上面的三个大图标; CommonImg里面,存放其他工具图标。 BigImg单个数组元素的结构如下: [javascript] view plain copy <span style="font-family:SimSun;">{ "title": "微信清理", "description": "定期清理微信,节省手机空间", "bigImg": { "ImgPosition": { "x": "0px", "y": "0px" } }, "commonImg": { "ImgPosition": { "x": "-100px", "y": "0px" } } },</span> 前两个属性看值就知道了; bingImg和commonImg属性中的ImgPosition中的两个属性,主要是描述这个图标在图片中的位置; CommonImg结构类似: [javascript] view plain copy { "title": "手游模拟器", "description": "电脑玩手游,挂机辅助神器", "type": "title", "commonImg": { "ImgPosition": { "x": "-100px", "y": "-100px" } } }, 只不过少了一个bigImg属性(因为他不需要); 但多了一个type属性,用于描述其将放置于哪个分类下面。 下面上JSON的全部内容:(共计661行) [javascript] view plain copy [ { "BigImg": [ { "title": "微信清理", "description": "定期清理微信,节省手机空间", "bigImg": { "ImgPosition": { "x": "0px", "y": "0px" } }, "commonImg": { "ImgPosition": { "x": "-100px", "y": "0px" } } }, { "title": "雷电OS", "description": "雷电OS Editor 旧机变新机", "bigImg": { "ImgPosition": { "x": "-350px", "y": "0px" } }, "commonImg": { "ImgPosition": { "x": "-600px", "y": "-500px" } } }, { "title": "手机相册扩容", "description": "无损处理图片,腾出50%空间", "bigImg": { "ImgPosition": { "x": "-700px", "y": "0px" } }, "commonImg": { "ImgPosition": { "x": "-700px", "y": "-500px" } } } ], "CommonImg": [ { "title": "手游模拟器", "description": "电脑玩手游,挂机辅助神器", "type": "title", "commonImg": { "ImgPosition": { "x": "-100px", "y": "-100px" } } }, { "title": "360连回家", "description": "随时随地,清理家中电脑", "type": "title", "commonImg": { "ImgPosition": { "x": "-200px", "y": "-100px" } } }, { "title": "驱动大师", "description": "驱动安装一键解决", "type": "title", "commonImg": { "ImgPosition": { "x": "-300px", "y": "-100px" } } }, { "title": "安全桌面", "description": "一键整理您的桌面", "type": "safe", "commonImg": { "ImgPosition": { "x": "-400px", "y": "-100px" } } }, { "title": "隐私保镖", "description": "五层隐私防护,清理隐私痕迹", "type": "safe", "commonImg": { "ImgPosition": { "x": "-500px", "y": "-100px" } } }, { "title": "防黑加固", "description": "修补可能会被黑客利用的", "type": "safe", "commonImg": { "ImgPosition": { "x": "-600px", "y": "-100px" } } }, { "title": "软件管家", "description": "安全下载,轻松管理您的软件", "type": "safe", "commonImg": { "ImgPosition": { "x": "-400px", "y": "0px" } } }, { "title": "人工服务", "description": "7*24小时为您解决电脑问题", "type": "safe", "commonImg": { "ImgPosition": { "x": "-500px", "y": "0px" } } }, { "title": "手机助手", "description": "免费手机应用,资源下载平台", "type": "safe", "commonImg": { "ImgPosition": { "x": "-700px", "y": "-100px" } } }, { "title": "弹窗拦截", "description": "拦弹窗,去广告,就是给力", "type": "safe", "commonImg": { "ImgPosition": { "x": "-800px", "y": "-100px" } } }, { "title": "隔离沙箱", "description": "隔离系统真实环境运行软件", "type": "safe", "commonImg": { "ImgPosition": { "x": "-900px", "y": "-100px" } } }, { "title": "软件小助手", "description": "快速启动您常用软件", "type": "safe", "commonImg": { "ImgPosition": { "x": "0px", "y": "-200px" } } }, { "title": "主页防护", "description": "防止恶意程序篡改浏览器主页", "type": "safe", "commonImg": { "ImgPosition": { "x": "-100px", "y": "-200px" } } }, { "title": "主页修复", "description": "一键解决浏览器主页相关问题", "type": "safe", "commonImg": { "ImgPosition": { "x": "-200px", "y": "-200px" } } }, { "title": "文件解密", "description": "免费还原被木马加密的文件", "type": "safe", "commonImg": { "ImgPosition": { "x": "-300px", "y": "-200px" } } }, { "title": "网络优化", "description": "全新家庭网络管理,秒踢蹭网", "type": "network", "commonImg": { "ImgPosition": { "x": "-400px", "y": "-200px" } } }, { "title": "断网急救箱", "description": "上不了网?就用断网急救箱", "type": "network", "commonImg": { "ImgPosition": { "x": "-600px", "y": "0px" } } }, { "title": "免费WiFi", "description": "电脑变热点,免费无线上网", "type": "network", "commonImg": { "ImgPosition": { "x": "-700px", "y": "0px" } } }, { "title": "宽带测速器", "description": "获取网络带宽和上网速度数值", "type": "network", "commonImg": { "ImgPosition": { "x": "-800px", "y": "0px" } } }, { "title": "WiFi体检", "description": "检测并修复路由器安全隐患", "type": "network", "commonImg": { "ImgPosition": { "x": "-500px", "y": "-200px" } } }, { "title": "流量防火墙", "description": "发现并阻止偷偷占流量的程序", "type": "network", "commonImg": { "ImgPosition": { "x": "0px", "y": "-100px" } } }, { "title": "LSP修复", "description": "修复网络异常和不能上网", "type": "network", "commonImg": { "ImgPosition": { "x": "-600px", "y": "-200px" } } }, { "title": "DNS优选", "description": "杜绝网络差,启用更优DNS", "type": "network", "commonImg": { "ImgPosition": { "x": "-700px", "y": "-200px" } } }, { "title": "寝室必备", "description": "看看哪个室友在占网速", "type": "network", "commonImg": { "ImgPosition": { "x": "-800px", "y": "-200px" } } }, { "title": "360壁纸", "description": "海量高清壁纸,美化电脑桌面", "type": "system", "commonImg": { "ImgPosition": { "x": "-900px", "y": "-200px" } } }, { "title": "急救盘", "description": "一盘在手,系统无忧", "type": "system", "commonImg": { "ImgPosition": { "x": "0px", "y": "-300px" } } }, { "title": "任务管理器", "description": "找出当前占用资源的程序", "type": "system", "commonImg": { "ImgPosition": { "x": "-300px", "y": "0px" } } }, { "title": "鲁大师", "description": "辨别硬件真伪,实时监测温度", "type": "system", "commonImg": { "ImgPosition": { "x": "-100px", "y": "-300px" } } }, { "title": "默认软件", "description": "帮您设置常用的默认软件", "type": "system", "commonImg": { "ImgPosition": { "x": "-200px", "y": "-300px" } } }, { "title": "查找大文件", "description": "找出占用磁盘空间的大文件", "type": "system", "commonImg": { "ImgPosition": { "x": "-300px", "y": "-300px" } } }, { "title": "注册表瘦身", "description": "清理无效、错误的注册表键值", "type": "system", "commonImg": { "ImgPosition": { "x": "-400px", "y": "-300px" } } }, { "title": "系统盘瘦身", "description": "通过瘦身解决系统盘空间不足", "type": "system", "commonImg": { "ImgPosition": { "x": "-500px", "y": "-300px" } } }, { "title": "文件恢复", "description": "快速帮您恢复被误删的文件", "type": "system", "commonImg": { "ImgPosition": { "x": "-600px", "y": "-300px" } } }, { "title": "系统急救箱", "description": "查杀顽固木马,修复系统异常", "type": "system", "commonImg": { "ImgPosition": { "x": "-600px", "y": "0px" } } }, { "title": "磁盘擦除", "description": "彻底清除磁盘数据,保护隐私", "type": "system", "commonImg": { "ImgPosition": { "x": "-700px", "y": "-300px" } } }, { "title": "一键装机", "description": "装机必备软件一键搞定", "type": "system", "commonImg": { "ImgPosition": { "x": "-800px", "y": "-300px" } } }, { "title": "右键管理", "description": "管理鼠标的右键菜单", "type": "system", "commonImg": { "ImgPosition": { "x": "-900px", "y": "-300px" } } }, { "title": "系统重装", "description": "无需光盘,恢复系统初始状态", "type": "system", "commonImg": { "ImgPosition": { "x": "0px", "y": "-400px" } } }, { "title": "网游加速器", "description": "解决游戏卡机掉线,永久免费", "type": "game", "commonImg": { "ImgPosition": { "x": "-100px", "y": "-400px" } } }, { "title": "游戏大厅", "description": "小号多开不串号,键鼠回放爽", "type": "game", "commonImg": { "ImgPosition": { "x": "-200px", "y": "-400px" } } }, { "title": "游戏保险箱", "description": "保护游戏网银账号安全", "type": "game", "commonImg": { "ImgPosition": { "x": "-300px", "y": "-400px" } } }, { "title": "游戏优化器", "description": "一键解决玩游戏卡、慢问题", "type": "game", "commonImg": { "ImgPosition": { "x": "-400px", "y": "-400px" } } }, { "title": "360理财", "description": "360互联网金融服务平台", "type": "smalltools", "commonImg": { "ImgPosition": { "x": "-500px", "y": "-400px" } } }, { "title": "360看图", "description": "防范图片木马,安全查看照片", "type": "smalltools", "commonImg": { "ImgPosition": { "x": "-600px", "y": "-400px" } } }, { "title": "魔法摄像头", "description": "让视频聊天既安全又有趣", "type": "smalltools", "commonImg": { "ImgPosition": { "x": "-700px", "y": "-400px" } } }, { "title": "360云盘", "description": "安全免费,超大空间的云盘", "type": "smalltools", "commonImg": { "ImgPosition": { "x": "-200px", "y": "0px" } } }, { "title": "C盘搬家", "description": "转移系统盘重要资料和文件", "type": "smalltools", "commonImg": { "ImgPosition": { "x": "-800px", "y": "-400px" } } }, { "title": "360问答", "description": "提出你的问题,分分钟有答案", "type": "smalltools", "commonImg": { "ImgPosition": { "x": "-900px", "y": "-400px" } } }, { "title": "苹果设备清理", "description": "清理App垃圾,节省手机空间", "type": "smalltools", "commonImg": { "ImgPosition": { "x": "0px", "y": "-500px" } } }, { "title": "360压缩", "description": "新一代的压缩软件,永久免费", "type": "smalltools", "commonImg": { "ImgPosition": { "x": "-100px", "y": "-500px" } } }, { "title": "健康精灵", "description": "可爱精灵,助您健康使用电脑", "type": "smalltools", "commonImg": { "ImgPosition": { "x": "-200px", "y": "-500px" } } }, { "title": "小清新日历", "description": "查询天气、农历和节假日", "type": "smalltools", "commonImg": { "ImgPosition": { "x": "0px", "y": "0px" } } }, { "title": "安全浏览器", "description": "恶意网站拦截,下载保护", "type": "smalltools", "commonImg": { "ImgPosition": { "x": "-300px", "y": "-500px" } } }, { "title": "文件粉碎机", "description": "彻底粉碎无法删除的文件", "type": "smalltools", "commonImg": { "ImgPosition": { "x": "-400px", "y": "-500px" } } }, { "title": "U盘鉴定器", "description": "鉴定U盘真实容量", "type": "smalltools", "commonImg": { "ImgPosition": { "x": "-500px", "y": "-500px" } } } ] } ] 为了适应type,接下来我们需要改造html模板: 将div.toolbox-all的dom结构改装成如下: [javascript] view plain copy div.toolbox-all //这个是最上面的大图标那一行 div.firstRow //以下是单个按钮 //横线那一行,如果是多行app,应考虑使用另外一个 div.dotted div.commonRow.title div.titleRow span.titleRow-left span.titleRow-text 电脑安全 div.commonRow.safe div.titleRow span.titleRow-left span.titleRow-text 网络优化 div.commonRow.network div.titleRow span.titleRow-left span.titleRow-text 系统工具 div.commonRow.system div.titleRow span.titleRow-left span.titleRow-text 游戏优化 div.commonRow.game div.titleRow span.titleRow-left span.titleRow-text 实用小工具 div.commonRow.smalltools 这样通过type来定位该图标被添加的dom位置即可。 顺便,以上订正了一个之前把commonRow打成commanRow的问题。记得修改样式表(尴尬) 再顺便订正三个样式需要被调整的地方: [css] view plain copy .back .contentbox .commonRow .normalTool .text .title { line-height: 25px; font-size: 16px; } .back .contentbox .commonRow .normalTool .text .description { line-height: 30px; font-size: 12px; color: #aaa; } .back .contentbox .commonRow .normalTool .addButton { display: none; position: absolute; top: 7px; right: 15px; width: 60px; height: 22px; background-image: linear-gradient(rgb(98, 227, 25) 0%, rgb(68, 208, 27) 100%); font-size: 12px; color: white; text-align: center; line-height: 20px; border: 1px solid rgb(65, 199, 36); -webkit-border-radius: 1px; -moz-border-radius: 1px; border-radius: 1px; } 下来呢,我们需要读取JSON,然后将其添加入页面之中; 首先,创建一个Tool类,他表示一个图标; [javascript] view plain copy //单个工具 var Tool = function (obj) { this.obj = obj; // 0表示未加载到我的工具,1表示加载到我的工具,2表示加载到我的工具的右下小窗处 // 为了方便测试,这里先默认设置为1 this.state = 1; //用于在全部工具页面 this.createBigImgDom = function (callback) { var self = this; var obj = this.obj var str = '<div class="BigTool">' + '<span class="img" style="background-position: ' + obj.bigImg.ImgPosition.x + ' ' + obj.bigImg.ImgPosition.y + '"></span>' + '<span class="mask"></span>' + '<div class="text">' + '<div class="title">' + obj.title + '</div>' + '<div class="description">' + obj.description + '</div>' + '</div>' + '<div class="addButton">添加</div>' + '</div>'; var node = $(str); node.click(function () { if (self.state) { callback(); } }) return node; }; this.createNormalTool = function (callback) { var self = this; var obj = this.obj var str = '<div class="normalTool">' + '<div class="img" style="background-position: ' + obj.commonImg.ImgPosition.x + ' ' + obj.commonImg.ImgPosition.y + '"></div>' + '<div class="text">' + '<div class="title">' + obj.title + '</div>' + '<div class="description">' + obj.description + '</div>' + '</div>' + '<div class="addButton">添加</div>' + '</div>'; var node = $(str); node.click(function () { if (self.state) { callback(); } }) return node; }; this.createSmallTool = function (callback) { var obj = this.obj var position_x = parseInt(obj.commonImg.ImgPosition.x) * 0.615 + "px"; var position_y = parseInt(obj.commonImg.ImgPosition.y) * 0.615 + "px"; var str = '<div class="tool-foot">' + '<div class="img" style="background-position: ' + position_x + ' ' + position_y + '"></div>' + '<div class="text"></div>' + '</div>'; var node = $(str); node.click(function () { if (self.state) { callback(); } }) return node; }; } 他有三个方法,两个属性; ①obj属性是在创建的时候赋值给他的,方便读取创建实例时的初始值。这个初始值就是上面那个JSON中的一个元素(BigImg或CommonImg中的一个元素); ②state属性表示该按钮状态,具体看注释 三个方法的作用依次为: ①返回一个用于所有工具最顶端的大图标的dom; ②返回一个用于放置在所有工具、我的工具普通位置的dom; ③返回一个用于放在我的工具右下角小位置的dom; ④他们都有一个点击事件,会判断当前状态来进行。为了方便测试,我这里并没有针对性的设置。在之后会进行修改。 我们还缺一些其他的方法,例如将移动用的函数,点击后触发事件的函数等等; 还缺一些属性,例如,设置其目前是否可以移动,目前处于什么位置的东西等等; 等等我们再补全这个Tool类。 然后,我们需要创建一个加载JSON,处理数据的类。 在创建这个类之前,我们建立一个data文件夹,和img、javascripts、stylesheets文件夹平级; 将JSON命名为tools.json,并放于data文件夹中; 下面是处理这个JSON的JS代码类: [javascript] view plain copy var ToolsConfigJsonLoad = function (url) { this.url = url ? url : "data/tools.json"; this.load = function () { var self = this; $.ajax({ url: self.url, dataType: "json", type: "GET", success: function (data) { self.addToolsInToolbox_all(data); } }) }; //将内容添加到全部工具页面中 this.addToolsInToolbox_all = function (data) { var type = []; data[0].BigImg.forEach(function (obj) { var tool = new Tool(obj); var mixin = new MixinTool(tool); var callback = mixin.mixin() $(".firstRow").append(tool.createBigImgDom(callback)); }) data[0].CommonImg.forEach(function (obj) { if (type.indexOf(obj.type) < 0) { type.push(obj.type); } var tool = new Tool(obj); var mixin = new MixinTool(tool); var callback = mixin.mixin() $(".commonRow." + obj.type).append(tool.createNormalTool(callback)); }) this.addPlaceHolderWhenOnlyTwoToolsInToolbox_All(type); this.addDottedLineInToolbox_All(); }; // 这个目的是当某一行只有两个图标时,创造一个占位的图标 this.addPlaceHolderWhenOnlyTwoToolsInToolbox_All = function (type) { type.forEach(function (obj) { var length = $(".commonRow." + obj + " > *").length; if (length % 3 == 2) { $(".commonRow." + obj).append($('<div class="normalToolHolder"></div>')); } }) }; // 这个目的是为了给全部工具中的多行工具之间添加分割线 this.addDottedLineInToolbox_All = function () { $(".commonRow .normalTool:nth-child(3n+4)").before('<div class="dotted"></div>'); } } 这部分代码最重要的是load函数; ①他会发起一个ajax请求,来读取这个json; ②ajax请求可以使用用户自己给的url(如果有的话),或者默认url; ③在请求成功后,会对数据进行处理,简单来说,分别遍历BigImg这个属性的每个元素以及CommonImg这个属性的每个元素; ④利用这些元素的数据,生成一个Tool实例,然后又对她做了一些其他事情(具体之后再说),然后生成一个dom对象,插入到指定位置。 在生成工具的时候,显然每个工具的用途是不同的,因此我们希望这个工具在点击的时候,执行不同的处理方法,具体做法有以下几种。 ①生成Tool实例的时候,手动给起赋予一个处理函数(缺点:生成代码虎非常长,而且堆积在一起会很乱); ②将处理函数放在Tool类里,然后生成dom的时候,点击事件触发Tool了的不同方法(缺点:Tool类会非常长,并且可以通过Tool类的实例来调用本来不希望他调用的方法); ③将所有的处理方法集中在一个类之中,我们需要编辑的时候只需要编辑这个类即可,然后将对应的方法,作为回调函数传递给这个dom的创建函数,在创建函数里,调用这个回调函数(我的选择)。 我这里选择的是第三种方法,因此需要生成一个MixinTool类,他具备将对应的方法返回给对应的Tool类实例的功能。 为了区别不同按钮,我在JSON里每个元素里新加了一个属性ID,具体修改后内容如下; [javascript] view plain copy { "title": "微信清理", "ID":"No0", "description": "定期清理微信,节省手机空间", "bigImg": { "ImgPosition": { "x": "0px", "y": "0px" } }, "commonImg": { "ImgPosition": { "x": "-100px", "y": "0px" } } }, 这里的ID的值,就是利用该元素生成Tool实例时,我们写在MixinTool类里,该元素预期拿取的点击事件处理函数。 MixinTool类的代码如下: [javascript] view plain copy var MixinTool = function (tool) { this.mixin = function () { var self = this; if ("ID" in tool.obj & tool.obj.ID in this) { //console.log(self[tool.obj.ID]) return self[tool.obj.ID]; } else { return self.default; } }; this.default = function () { console.log("No thing will happen"); } this.No0 = function () { console.log("No 0 you click it"); }; this.No1 = function () { console.log("No 1 you click it"); }; this.No2 = function () { console.log("No 2 you click it"); }; this.No3 = function () { console.log("No 3 you click it"); }; this.No4 = function () { console.log("No 4 you click it"); }; this.No5 = function () { console.log("No 5 you click it"); }; } 他有一些方法,假如某个Tool实例没有对应的方法,他会执行default这个函数作为点击的响应事件;否则执行对应的。 mixin函数需要显示调用,作为Tool类创建dom结点时的参数使用。 具体如何使用参照上面的例子。 由于我们已经抽象出来多个类了,因此不如将之前页面切换的逻辑也抽象成一个类,具体代码如下: [javascript] view plain copy //注意,这些其实都是全局变量 var Tab = function () { //以下代码大量考虑到扩展性,例如,可以新增一个tab和content页面 this.tabClick = function () { $(".tool").click(function () { //这里是上面的图标的逻辑变换 if (!($(this.children[0]).hasClass("select"))) { $(".select").removeClass("select"); $(this.children[0]).addClass("select"); //这里是hover的横线的位置变化 var node = $(".tool .hover"); node.remove(); //当动画需要停止的时候,让他停止 if ('stop' in node) { node.stop(); } node.css("left", "0px"); $(this).append(node); //以下应该是切换页面的逻辑 //获取切换到哪一个页面, var index = null; for (var i = 0; i < this.parentNode.children.length; i++) { if (this == this.parentNode.children[i]) { index = i; } } $(".contentbox > div").addClass("displayNONE"); $(".contentbox > div:nth-child(" + (index + 1) + ")").removeClass("displayNONE"); } }) }; this.tabMouseEnter = function () { $(".tool").mouseenter(function (evt) { //只有当鼠标移动到非当前选中的tab上时,才会移动 if (!($(this.children[0]).hasClass("select"))) { var self = this; var node = $(".tool .hover"); var start = null; var end = null; var tools = $(".toolTab")[0].children for (var i = 0; i < tools.length; i++) { if (self == tools[i]) { end = i; } else if ($(".select")[0].parentNode == tools[i]) { start = i; } } //停止之前的动画 if ('stop' in node) { node.stop(); } //现在开始动画效果 node.animate({"left": (end - start) * 160 + "px"}) } }) }; this.tabMouseLeave = function () { $(".tool").mouseleave(function () { //只有当鼠标移动到非当前选中的tab上时,才会移动 if (!($(this.children[0]).hasClass("select"))) { var node = $(".tool .hover"); //停止之前的动画 if ('stop' in node) { node.stop(); } node.animate({"left": "0px"}) } }) } } 而调用到目前为止的类和函数,十分简单,如代码: [javascript] view plain copy $(document).ready(function () { //这里是点击切换显示页面 var toolboxTab = new Tab(); toolboxTab.tabClick(); toolboxTab.tabMouseEnter(); toolboxTab.tabMouseLeave(); var jsonLoad = new ToolsConfigJsonLoad(); jsonLoad.load(); }) 目前进度: ①自动生成所有工具里的所有工具; ②给工具添加点击响应事件; 目前还欠缺的内容: ①将所有工具里的工具,添加进我的工具; ②我的工具页面的各种逻辑; ③视情况,让工具可以被添加、或不能被添加(添加按钮在已添加后禁止显示)。
(三)我的工具页面布局 如图: 首先将其分为二部分; 第一部分是上方整体红色方框区域; 包含若干个独立按钮,按钮分为图片和下方文字两部分; 第二部分是下方蓝色方框区域; 包含左方的编辑按钮和右方的四个快捷按钮区域; 左方是图标和文字,图标分为按下和非按下状态; 右方是左边的文字和右侧的按钮,按钮又分为图标和文字。按钮在无图标时有占位图标。 先上模板: [javascript] view plain copy //我的工具,和之前的div.toolbox-all平级 div.toolbox-my.displayNONE //上方区域 div.toolbox-content //独立按钮 div.tool-my div.img div.text 小清新日历 //下方区域 div.toolbox-foot //编辑按钮 div.edit div.img div.text 编辑 //右方区域 div.shortcut //左边的描述文字 div.description div.text 主界面快捷入口: //右边四个按钮 div.tool-foot div.img div.text 系统急救箱 div.tool-foot.placeholder div.placeholder-img div.text 拖拽到此 div.tool-foot.placeholder div.placeholder-img div.text 拖拽到此 div.tool-foot.placeholder div.placeholder-img div.text 拖拽到此 然后是CSS的样式:(会涉及图片,后补,图标图片除外) [css] view plain copy .back .contentbox .toolbox-my { background-color: white; padding: 30px 40px 90px 40px; position: relative; } .back .contentbox .toolbox-my .toolbox-content { width: 100%; height: 100%; overflow-x: hidden; overflow-y: auto; display: flex; flex-wrap: wrap; } .back .contentbox .toolbox-my .toolbox-content .tool-my { width: 100px; height: 100px; display: inline-block; position: relative; border: 1px solid transparent; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; } .back .contentbox .toolbox-my .toolbox-content .tool-my:hover { -webkit-border-radius: 2px; -moz-border-radius: 2px; border-radius: 2px; border: 1px solid #DADADA; } .back .contentbox .toolbox-my .toolbox-content .tool-my .img { position: absolute; top: 18px; left: 23px; right: 23px; bottom: 28px; background-image: url("../img/toolsImg.png"); background-position: 0 0; } .back .contentbox .toolbox-my .toolbox-content .tool-my .text { position: absolute; bottom: 9px; width: 100%; text-align: center; line-height: 12px; height: 12px; font-size: 12px; color: #7c7c7c; } .back .contentbox .toolbox-my .toolbox-foot { position: absolute; left: 0; right: 0; bottom: 0; height: 95px; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; border-top: 1px solid rgb(218, 218, 218); } .back .contentbox .toolbox-my .toolbox-foot .edit { width: 32px; height: 52px; position: absolute; top: 22px; left: 30px; } .back .contentbox .toolbox-my .toolbox-foot .edit .img { width: 32px; height: 32px; background-image: url(../img/toolbox.png); background-position: -120px 0; } .back .contentbox .toolbox-my .toolbox-foot .edit .img:hover { background-position: -120px -50px; } .back .contentbox .toolbox-my .toolbox-foot .edit .text { width: 32px; height: 20px; line-height: 20px; vertical-align: bottom; color: rgb(0, 138, 225); font-size: 12px; text-align: center; cursor: default; } .back .contentbox .toolbox-my .toolbox-foot .shortcut { position: absolute; right: 13px; top: 9px; bottom: 15px; width: 450px; display: flex; justify-content: flex-end; align-items: flex-end; } .back .contentbox .toolbox-my .toolbox-foot .shortcut .description { width: 98px; height: 22px; } .back .contentbox .toolbox-my .toolbox-foot .shortcut .description .text { height: 22px; line-height: 22px; font-size: 11px; color: #7c7c7c; vertical-align: top; } .back .contentbox .toolbox-my .toolbox-foot .shortcut .tool-foot { width: 76px; height: 70px; border: 1px solid transparent; -webkit-border-radius: 2px; -moz-border-radius: 2px; border-radius: 2px; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; position: relative; } .back .contentbox .toolbox-my .toolbox-foot .shortcut .tool-foot:hover { border: 1px solid #dadada; } .back .contentbox .toolbox-my .toolbox-foot .shortcut .tool-foot .img { position: absolute; top: 13px; left: 21px; right: 21px; height: 34px; -webkit-border-radius: 2px; -moz-border-radius: 2px; border-radius: 2px; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; border: 1px solid transparent; background-image: url("../img/toolsImg.png"); background-size: 615px 615px; /* 这个用于计算位置,和实际位置需要乘以61.5% background-position: 0 0; */ } .back .contentbox .toolbox-my .toolbox-foot .shortcut .tool-foot .text { position: absolute; bottom: 3px; width: 100%; text-align: center; line-height: 12px; height: 12px; font-size: 12px; color: #7c7c7c; } .back .contentbox .toolbox-my .toolbox-foot .shortcut .tool-foot.placeholder:hover { border: 1px solid transparent; } .back .contentbox .toolbox-my .toolbox-foot .shortcut .tool-foot.placeholder .placeholder-img { position: absolute; top: 13px; left: 21px; right: 21px; height: 34px; -webkit-border-radius: 2px; -moz-border-radius: 2px; border-radius: 2px; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; border: 1px dotted #7c7c7c; } 然后对之前的一些css和html修改: 首先,删除掉 [javascript] view plain copy div.BigTool span.img(style='background-image: url(../img/bigImg03.png)') 注意,以上共三个,最后的图片链接不同,修改为: [javascript] view plain copy div.BigTool span.img 我们将加载一个大图片,然后实际生成的时候,设置background-position属性,来设置其加载哪个图片 修改以下同名样式为: [css] view plain copy .back .contentbox .toolbox-all .firstRow .BigTool .img { display: inline-block; position: absolute; width: 100%; height: 100%; background-image: url("../img/bigImg.png"); background-position: 0 0;; } 然后修改该样式: [css] view plain copy .back .contentbox .commanRow .normalTool .img { position: relative; display: inline-block; width: 60px; height: 60px; background-image: url("../img/toolsImg.png"); background-position: 0 0;; } 于是,我们需要三个图片: toolbox.png 放零碎的图标, bigImg.png 放大图标,图标尺寸为300x160 toolsImg.png 放普通图标,尺寸为52x52 我自己已经切好了(话说切图好无聊),下载链接为: http://jianwangsan.cn/img/toolbox.png http://jianwangsan.cn/img/bigImg.png http://jianwangsan.cn/img/toolsImg.png 放在img文件夹之内食用 目前效果应该如下: 图片之所以重复,是因为使用的是默认第一个位置的图片,在实际生成的时候,会进行修改。 demo链接: http://jianwangsan.cn/toolbox 这个页面做完,主要部分的页面就做完啦~~当然,这只是模板,具体生成内容,会在第四部分通过js来读取json而生成,读取JSON生成的好处,在于日后无论添加、删除或者修改图标,甚至逻辑,都很容易。
DEMO: http://jianwangsan.cn/toolbox (二)全部工具里面的按钮和样式 我将他拆成五部分: 第一部分是上面的大按钮那一排; 第二部分是小按钮; 第三部分是一条颜色很淡的线,他只在app有多行的情况下,在行间有; 第四部分是标题; 第五部分是右边的滚动条; ①为了方便模块化开发,我先制作模板,然后实际编写代码的时候,根据样式已经设置好的模板,来添加内容,这样方便调试和制作 ②大按钮: 先分为整体和具体每个按钮; 每个按钮分为背景图、遮罩层(下半部分的灰色阴影),文字title,文字说明,还有移动上去可能显示的添加按钮,共计五部分。 html: [javascript] view plain copy div.contentbox //下面这个是全部工具里页面最顶级的,目前共有两个,通过displayNONE这个类是否存在,来决定显示哪个 div.toolbox-all //这个是最上面的大图标那一行 div.firstRow //以下是单个按钮 div.BigTool //背景图 span.img(style='background-image: url(../img/bigImg01.png)') //阴影遮罩 span.mask //文字 div.text div.title 微信清理 div.description 定期清理微信,节省手机空间 //添加按钮 div.addButton 添 加 div.BigTool span.img(style='background-image: url(../img/bigImg02.png)') span.mask div.text div.title 雷电OS div.description 雷电OS Editor 旧机变新机 div.addButton 添 加 div.BigTool span.img(style='background-image: url(../img/bigImg03.png)') span.mask div.text div.title 手机相册扩容 div.description 无损处理照片,腾出50%空间 div.addButton 添 加 CSS [css] view plain copy .back .contentbox > div { width: 100%; height: 100%; box-sizing: border-box; } .back .contentbox .toolbox-all { background: white; padding: 30px; overflow: auto; } .back .contentbox .toolbox-my { background: green; } .back .contentbox .toolbox-all .firstRow { width: 100%; height: 140px; display: flex; justify-content: space-between; } .back .contentbox .toolbox-all .firstRow .BigTool { width: 300px; height: 140px; position: relative; cursor: pointer; } .back .contentbox .toolbox-all .firstRow .BigTool .img { display: inline-block; position: absolute; width: 100%; height: 100%; } .back .contentbox .toolbox-all .firstRow .BigTool .mask { display: inline-block; position: absolute; width: 100%; height: 100%; background-image: linear-gradient(rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0) 40%, rgba(0, 0, 0, 0.5) 60%, rgba(0, 0, 0, 0.8) 100%); } .back .contentbox .toolbox-all .firstRow .BigTool .text { position: absolute; bottom: 0; left: 0; right: 0; height: 55px; padding: 0 10px; } .back .contentbox .toolbox-all .firstRow .BigTool .text .title { font-weight: 600; font-size: 18px; color: white; } .back .contentbox .toolbox-all .firstRow .BigTool .text .description { font-size: 14px; margin-top: 10px; color: rgb(218, 218, 218); } .back .contentbox .toolbox-all .firstRow .BigTool .addButton { display: none; position: absolute; bottom: 10px; right: 12px; width: 60px; height: 22px; background-image: linear-gradient(rgb(98, 227, 25) 0%, rgb(68, 208, 27) 100%); font-size: 12px; color: white; text-align: center; line-height: 20px; border: 1px solid rgb(65, 199, 36); -webkit-border-radius: 1px; -moz-border-radius: 1px; border-radius: 1px; } .back .contentbox .toolbox-all .firstRow .BigTool:hover .addButton { display: block; } ③小按钮: 分为左边的图片,右边的文字(title和description),添加按钮 所有小按钮放在同一个commonRow类里面,这个commonRow是弹性布局。 commonRow和上面的firstRow是平级的 HTML: [javascript] view plain copy div.commanRow div.normalTool div.img div.text div.title 手游模拟器 div.description 电脑玩手游,挂机辅助神器 div.addButton 添 加 div.normalTool div.img div.text div.title 手游模拟器 div.description 电脑玩手游,挂机辅助神器 div.addButton 添 加 div.normalTool div.img div.text div.title 手游模拟器 div.description 电脑玩手游,挂机辅助神器 div.addButton 添 加 div.normalTool div.img div.text div.title 手游模拟器 div.description 电脑玩手游,挂机辅助神器 div.addButton 添 加 CSS [css] view plain copy .back .contentbox .commanRow { width: 100%; display: flex; justify-content: space-between; margin-top: 10px; flex-flow: wrap; } .back .contentbox .commanRow .normalTool { width: 300px; height: 70px; position: relative; cursor: pointer; padding: 5px; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; } .back .contentbox .commanRow .normalTool:hover { outline: 1px solid #dadada; } .back .contentbox .commanRow .normalTool .img { position: relative; display: inline-block; width: 60px; height: 60px; background-color: blue; } .back .contentbox .commanRow .normalTool .text { position: absolute; left: 75px; right: 5px; top: 5px; bottom: 5px; } .back .contentbox .commanRow .normalTool .text .title { line-height: 35px; font-size: 16px; } .back .contentbox .commanRow .normalTool .text .description { line-height: 25px; font-size: 12px; color: #aaa; } .back .contentbox .commanRow .normalTool .addButton { display: none; position: absolute; top: 10px; right: 15px; width: 60px; height: 22px; background-image: linear-gradient(rgb(98, 227, 25) 0%, rgb(68, 208, 27) 100%); font-size: 12px; color: white; text-align: center; line-height: 20px; border: 1px solid rgb(65, 199, 36); -webkit-border-radius: 1px; -moz-border-radius: 1px; border-radius: 1px; } .back .contentbox .commanRow .normalTool:hover .addButton { display: block; } ④一条颜色很淡的线,分为两种情况,顶层的,和间层的。 直接插入一个div,利用 border: 1px dotted rgb(209, 209, 209); 属性来设置, html: div.dotted 在顶层的时候,和firstRow平级,在间层的时候,和.normalTool平级 css: [javascript] view plain copy .back .contentbox .dotted { border: 1px dotted rgb(209, 209, 209); margin-top: 10px; width: 100%; margin-bottom: 10px; } ⑤标题: 两部分,左边的绿色竖线,右边的文字,简单暴力 html [javascript] view plain copy div.titleRow span.titleRow-left span.titleRow-text 电脑安全 CSS: [css] view plain copy .back .contentbox .titleRow { width: 100%; height: 20px; margin-top: 25px; } .back .contentbox .titleRow .titleRow-left { display: inline-block; width: 2px; height: 100%; background-color: rgb(42, 191, 29); } .back .contentbox .titleRow .titleRow-text { height: 100%; display: inline-block; line-height: 20px; margin-left: 10px; vertical-align: top; font-size: 18px; color: #000; } ⑥滚动条 分为四部分:滚动条整体、滚动条背景、滚动条、两端的按钮 不需要html代码; CSS代码限制为当前页面生效: [css] view plain copy /* 有这行才有效,滚动条的宽度 */ .back .contentbox ::-webkit-scrollbar { width: 12px; } /* 滚动条的背景 */ .back .contentbox ::-webkit-scrollbar-track { background-color: rgb(242, 242, 242); } /*滚动条*/ .back .contentbox ::-webkit-scrollbar-thumb { -webkit-border-radius: 5px; border-radius: 5px; background: rgb(218, 218, 218); } .back .contentbox ::-webkit-scrollbar-button { width: 12px; } 总结: 目前情况是小按钮没有背景图,暂时不做,只起到占位效果,背景图等做按钮类时,再根据实际需要设置和添加。 后附目前为止的代码: HTML:(jade格式) [javascript] view plain copy extends layout block content link(rel='stylesheet', href='./stylesheets/toolboxes.css') script(type="text/javascript",src='javascripts/toolboxes.js') div 博客地址: a(href='http://blog.csdn.net/qq20004604?viewmode=list' target='_blank') http://blog.csdn.net/qq20004604?viewmode=list br br div.back div.tooltop div.tooltop-img div.toolTab div.tool#alltool span.img.alltool.select span.text 全部工具 div.hover div.tool#mytool span.img.mytool span.text 我的工具 div.contentbox //下面这个是全部工具里页面最顶级的,目前共有两个,通过displayNONE这个类是否存在,来决定显示哪个 div.toolbox-all //这个是最上面的大图标那一行 div.firstRow //以下是单个按钮 div.BigTool //背景图 span.img(style='background-image: url(../img/bigImg01.png)') //阴影遮罩 span.mask //文字 div.text div.title 微信清理 div.description 定期清理微信,节省手机空间 //添加按钮 div.addButton 添 加 div.BigTool span.img(style='background-image: url(../img/bigImg02.png)') span.mask div.text div.title 雷电OS div.description 雷电OS Editor 旧机变新机 div.addButton 添 加 div.BigTool span.img(style='background-image: url(../img/bigImg03.png)') span.mask div.text div.title 手机相册扩容 div.description 无损处理照片,腾出50%空间 div.addButton 添 加 //横线那一行,如果是多行app,应考虑使用另外一个 div.dotted div.commanRow div.normalTool div.img div.text div.title 手游模拟器 div.description 电脑玩手游,挂机辅助神器 div.addButton 添 加 div.normalTool div.img div.text div.title 手游模拟器 div.description 电脑玩手游,挂机辅助神器 div.addButton 添 加 div.normalTool div.img div.text div.title 手游模拟器 div.description 电脑玩手游,挂机辅助神器 div.addButton 添 加 div.dotted div.normalTool div.img div.text div.title 手游模拟器 div.description 电脑玩手游,挂机辅助神器 div.addButton 添 加 div.titleRow span.titleRow-left span.titleRow-text 电脑安全 div.commanRow div.normalTool div.img div.text div.title 手游模拟器 div.description 电脑玩手游,挂机辅助神器 div.addButton 添 加 div.toolbox-my.displayNONE CSS代码: [javascript] view plain copy a[href='/toolbox'] { color: #555; text-decoration: none; background-color: #e5e5e5; -webkit-box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.125); -moz-box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.125); box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.125); } .back { height: 600px; width: 100%; position: relative; -webkit-box-shadow: 0px 0px 2px #555; -moz-box-shadow: 0px 0px 2px #555; box-shadow: 0px 0px 2px #555; min-width: 1000px; } .back * { border: 0; padding: 0; margin: 0; } .back .tooltop { height: 120px; width: 100%; background-color: white; position: relative; } .back .tooltop-img { height: 100%; width: 100%; background-image: url(../img/toolboxBackground.png); background-size: cover; } .back .toolTab { position: absolute; left: 0; bottom: 10px; height: 35px; width: 100%; } .back .toolTab .tool { margin-left: 20px; width: 140px; height: 100%; line-height: 30px; color: white; font-size: 22px; font-weight: 900; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; display: inline-block; cursor: default; position: relative; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } .back .toolTab .tool .img { height: 27px; width: 27px; background-repeat: no-repeat; display: inline-block; vertical-align: middle; background-image: url(../img/toolbox.png); } .back .toolTab .tool .img.alltool { background-position: 0 0; } .back .toolTab .tool .img.alltool.select { background-position: 0 -50px; } .back .toolTab .tool .img.mytool { background-position: -40px 0; } .back .toolTab .tool .img.mytool.select { background-position: -40px -50px; } .back .toolTab .tool .text { } .back .toolTab .hover { height: 2px; width: 125px; background-color: white; position: absolute; bottom: -2px; left: 0; } .back .contentbox { position: absolute; top: 120px; bottom: 0; left: 0; right: 0; background-color: white; } .back .contentbox > div { width: 100%; height: 100%; box-sizing: border-box; } .back .contentbox .toolbox-all { background: white; padding: 30px; overflow: auto; } .back .contentbox .toolbox-my { background: green; } .back .contentbox .toolbox-all .firstRow { width: 100%; height: 140px; display: flex; justify-content: space-between; } .back .contentbox .toolbox-all .firstRow .BigTool { width: 300px; height: 140px; position: relative; cursor: pointer; } .back .contentbox .toolbox-all .firstRow .BigTool .img { display: inline-block; position: absolute; width: 100%; height: 100%; } .back .contentbox .toolbox-all .firstRow .BigTool .mask { display: inline-block; position: absolute; width: 100%; height: 100%; background-image: linear-gradient(rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0) 40%, rgba(0, 0, 0, 0.5) 60%, rgba(0, 0, 0, 0.8) 100%); } .back .contentbox .toolbox-all .firstRow .BigTool .text { position: absolute; bottom: 0; left: 0; right: 0; height: 55px; padding: 0 10px; } .back .contentbox .toolbox-all .firstRow .BigTool .text .title { font-weight: 600; font-size: 18px; color: white; } .back .contentbox .toolbox-all .firstRow .BigTool .text .description { font-size: 14px; margin-top: 10px; color: rgb(218, 218, 218); } .back .contentbox .toolbox-all .firstRow .BigTool .addButton { display: none; position: absolute; bottom: 10px; right: 12px; width: 60px; height: 22px; background-image: linear-gradient(rgb(98, 227, 25) 0%, rgb(68, 208, 27) 100%); font-size: 12px; color: white; text-align: center; line-height: 20px; border: 1px solid rgb(65, 199, 36); -webkit-border-radius: 1px; -moz-border-radius: 1px; border-radius: 1px; } .back .contentbox .toolbox-all .firstRow .BigTool:hover .addButton { display: block; } .back .contentbox .dotted { border: 1px dotted rgb(209, 209, 209); margin-top: 10px; width: 100%; margin-bottom: 10px; } .back .contentbox .commanRow { width: 100%; display: flex; justify-content: space-between; margin-top: 10px; flex-flow: wrap; } .back .contentbox .commanRow .normalTool { width: 300px; height: 70px; position: relative; cursor: pointer; padding: 5px; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; } .back .contentbox .commanRow .normalTool:hover { outline: 1px solid #dadada; } .back .contentbox .commanRow .normalTool .img { position: relative; display: inline-block; width: 60px; height: 60px; background-color: blue; } .back .contentbox .commanRow .normalTool .text { position: absolute; left: 75px; right: 5px; top: 5px; bottom: 5px; } .back .contentbox .commanRow .normalTool .text .title { line-height: 35px; font-size: 16px; } .back .contentbox .commanRow .normalTool .text .description { line-height: 25px; font-size: 12px; color: #aaa; } .back .contentbox .commanRow .normalTool .addButton { display: none; position: absolute; top: 10px; right: 15px; width: 60px; height: 22px; background-image: linear-gradient(rgb(98, 227, 25) 0%, rgb(68, 208, 27) 100%); font-size: 12px; color: white; text-align: center; line-height: 20px; border: 1px solid rgb(65, 199, 36); -webkit-border-radius: 1px; -moz-border-radius: 1px; border-radius: 1px; } .back .contentbox .commanRow .normalTool:hover .addButton { display: block; } .back .contentbox .titleRow { width: 100%; height: 20px; margin-top: 25px; } .back .contentbox .titleRow .titleRow-left { display: inline-block; width: 2px; height: 100%; background-color: rgb(42, 191, 29); } .back .contentbox .titleRow .titleRow-text { height: 100%; display: inline-block; line-height: 20px; margin-left: 10px; vertical-align: top; font-size: 18px; color: #000; } /* 有这行才有效,滚动条的宽度 */ .back .contentbox ::-webkit-scrollbar { width: 12px; } /* 滚动条的背景 */ .back .contentbox ::-webkit-scrollbar-track { background-color: rgb(242, 242, 242); } /*滚动条*/ .back .contentbox ::-webkit-scrollbar-thumb { -webkit-border-radius: 5px; border-radius: 5px; background: rgb(218, 218, 218); } .back .contentbox ::-webkit-scrollbar-button { width: 12px; } JS代码: 无新增,参照上一篇,略。
需求: ①写一个web版的360工具箱,示意图如下: ②无左上返回按钮,右上按钮有皮肤切换,下拉框(但无点击逻辑); ③按钮点击有事件,但事件是console.log(按钮名); ④可以在全部工具和我等工具自由切换; ⑤可以点击左下角的编辑,然后根据实际表现设置; ⑥可以在全部工具里面,点击按钮,然后添加到我的工具这边来; ⑦效果尽量与原图相同,只使用jquery库; 效果网址: http://jianwangsan.cn/toolbox (一)tab页切换,包含内容区 ①切图: 先切图,如图:(不想用他的绿色的) 再切按钮图(自行ps):(下图白色,所以直接是看不见的) ②页面制作: html:我这里发的是jade版,如果需要看html版的话,请打开demo的网址:http://jianwangsan.cn/toolbox,然后查看源代码自行复制 [javascript] view plain copy link(rel='stylesheet', href='./stylesheets/toolboxes.css') script(type="text/javascript",src='javascripts/toolboxes.js') div.back div.tooltop div.tooltop-img div.toolTab div.tool#alltool span.img.alltool.select span.text 全部工具 div.hover div.tool#mytool span.img.mytool span.text 我的工具 div.contentbox div.toolbox-all div.toolbox-my.displayNONE CSS: [css] view plain copy a[href='/toolbox'] { color: #555; text-decoration: none; background-color: #e5e5e5; -webkit-box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.125); -moz-box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.125); box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.125); } .back { height: 600px; width: 100%; position: relative; -webkit-box-shadow: 0px 0px 2px #555; -moz-box-shadow: 0px 0px 2px #555; box-shadow: 0px 0px 2px #555; } .back * { border: 0; padding: 0; margin: 0; } .back .tooltop { height: 120px; width: 100%; background-color: white; position: relative; } .back .tooltop-img { height: 100%; width: 100%; background-image: url(../img/toolboxBackground.png); background-size: cover; } .back .toolTab { position: absolute; left: 0; bottom: 10px; height: 35px; width: 100%; } .back .toolTab .tool { margin-left: 20px; width: 140px; height: 100%; line-height: 30px; color: white; font-size: 22px; font-weight: 900; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; display: inline-block; cursor: default; position: relative; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } .back .toolTab .tool .img { height: 27px; width: 27px; background-repeat: no-repeat; display: inline-block; vertical-align: middle; background-image: url(../img/toolbox.png); } .back .toolTab .tool .img.alltool { background-position: 0 0; } .back .toolTab .tool .img.alltool.select { background-position: 0 -50px; } .back .toolTab .tool .img.mytool { background-position: -40px 0; } .back .toolTab .tool .img.mytool.select { background-position: -40px -50px; } .back .toolTab .tool .text { } .back .toolTab .hover { height: 2px; width: 125px; background-color: white; position: absolute; bottom: -2px; left: 0; } .back .contentbox { position: absolute; top: 120px; bottom: 0; left: 0; right: 0; background-color: white; } .back .contentbox > div { width: 100%; height: 100%; } .back .contentbox .toolbox-all { background: red; } .back .contentbox .toolbox-my { background: green; } JavaScript: [javascript] view plain copy $(document).ready(function () { //这里是点击切换显示页面 var toolboxTab = new Tab(); toolboxTab.tabClick(); toolboxTab.tabMouseEnter(); toolboxTab.tabMouseLeave(); }) var Tab = function () { //以下代码大量考虑到扩展性,例如,可以新增一个tab和content页面 this.tabClick = function () { $(".tool").click(function () { //这里是上面的图标的逻辑变换 if (!($(this.children[0]).hasClass("select"))) { $(".select").removeClass("select"); $(this.children[0]).addClass("select"); //这里是hover的横线的位置变化 var node = $(".tool .hover"); node.remove(); //当动画需要停止的时候,让他停止 if ('stop' in node) { node.stop(); } node.css("left", "0px"); $(this).append(node); //以下应该是切换页面的逻辑 //获取切换到哪一个页面, var index = null; for (var i = 0; i < this.parentNode.children.length; i++) { if (this == this.parentNode.children[i]) { index = i; } } $(".contentbox > div").addClass("displayNONE"); $(".contentbox > div:nth-child(" + (index + 1) + ")").removeClass("displayNONE"); } }) }; this.tabMouseEnter = function () { $(".tool").mouseenter(function (evt) { //只有当鼠标移动到非当前选中的tab上时,才会移动 if (!($(this.children[0]).hasClass("select"))) { var self = this; var node = $(".tool .hover"); var start = null; var end = null; var tools = $(".toolTab")[0].children for (var i = 0; i < tools.length; i++) { if (self == tools[i]) { end = i; } else if ($(".select")[0].parentNode == tools[i]) { start = i; } } //停止之前的动画 if ('stop' in node) { node.stop(); } //现在开始动画效果 node.animate({"left": (end - start) * 160 + "px"}) } }) }; this.tabMouseLeave = function () { $(".tool").mouseleave(function () { //只有当鼠标移动到非当前选中的tab上时,才会移动 if (!($(this.children[0]).hasClass("select"))) { var node = $(".tool .hover"); //停止之前的动画 if ('stop' in node) { node.stop(); } node.animate({"left": "0px"}) } }) } } 到目前为止,tab按钮的动画和切换可以了,页面也可以正常切换了。 当然,目前页面颜色用的是纯色来站位,之后会修改
(86)apsect 模块:dojo/aspect 参数:apsect 【方法一】:aspect.after(对象, ”方法名”, 回调函数) 说明: 将在指定对象的方法执行结束后,执行回调函数; 例如,在点击 <div id="aa" style="width:100px;height:100px;background-color:green"></div> 这样一个dom后,会触发test对象的test方法; test方法的效果是弹窗,显示2; 而apsect会监听test对象的test方法,当他触发test方法后,在test方法执行完毕之后执行aspect中第三个参数的函数。 如示例: require(["dojo/aspect", "dojo/on", "dojo/dom", "dojo/domReady!"], function (aspect, on, dom) { on(dom.byId("aa"), "click", function () { test.test(); }) var test = { test: function () { alert("2"); return "1"; } } aspect.after(test, "test", function (arg) { alert(arg) }) }) 【方法二】:aspect.before(对象, ”方法名”, 回调函数) 与after相反,他将先执行aspect的回调函数,执行完毕之后再执行原方法。 例如在上面的例子中,更换为before,会导致先alert(undefined),再alert(“1”) 之所以会输出undefined 原因在于,这种情况下是不能获取test对象的test方法的返回值的。
(填坑中,预计7月底完成) 最后更新时间7/28晚, 更新页面基本样式已有 范例网址:121.41.66.68 【0】涉及到的框架、引擎、数据库: ① express 4.X ② jade ③ mysql 5.6.x 注: ①内容较长,我会尽力把整个框架、结构、顺序、思考方式说明白。 ②基础是《node.js开发指南》这本书,作者:BYVoid。但他书的版本较老,很多东西现在已经无法应用,故进行更新,使用目前普遍的express 4.x版本(原书似乎是2.X版本),mysql(原书是mongodb),jade(原书是ejs) 【1】基本需求: ①有首页; ②支持注册; ③支持登录、登出; ④可以发表博客,发表的博客可以保存到数据库; ⑤可以查看博客,博客从数据库中读取,为了简化,这里设置为查看所有人的博客; ⑥查看博客时,初始查看的数量有限,但可以无限加载新的博客,直到查看完全部博客; ⑦一定程度上实现多语言(即可以切换显示的语言版本),但由于复杂度所限,因此只部分实现(但框架已建立好,可以通过继续完善代码实现整体的国际化); ⑧根据登录状态,对可以访问的页面进行控制(某些允许某些禁止) 【2】前后端交互的简单过程: 【3】关于客户端请求的几种情况(涉及到数据库的) 【4】npm npm是包管理器,在新版本里是默认安装好的,可以输入: npm -v 来查看npm的版本 【5】express框架: 首先安装基础的express框架,他是封装好的web开发框架,里面包含了: ①路由控制、 ②log显示、 ③解析客户端请求、 ④cookies解析、 ⑤静态文件的控制 等多种功能。 安装前注: ①有的人只需要简单的 npm install -g express npm install -g express-generator 就可以愉快的跑起express了,有的人就像向我一样苦逼,尝试各种办法,最后勉强可以用。 如果在这两行命令后,输入(V是大写的) express -V 会返回版本号,那么直接跳到最后来看,如果不是这样,可以参考我写的记录来安装。 或者直接看后面的终极解决方案 ②express设置全局方法: ln -s /usr/nodejs4.4.7/node-v4.4.7-linux-x64/bin/express /usr/local/bin/express 其他需要全局的方法,理论上同理,即将nodejs安装目录下的bin文件夹下的模块,映射到/usr/local/bin/同名文件即可 ③express命令可以使用的人: 输入: express -t jade myblog 效果是建立一个文件夹名为myblog的文件夹,里面会有一些文件。 正常如下图: cd myblog npm install 这时,npm会根据package.json来安装一些东西。 按提示输入: SET DEBUG=myblog:* npm start 可以启动项目。 本机的话,通过访问http://127.0.0.1:3000/ 来查看效果 服务器的话,访问其公网ip,端口是3000 查看package.json "scripts": { "start": "node ./bin/www" }, 这条属性告诉我们,需要通过bin文件夹下的www来启动,这也就是上面npm start命令的作用。 www文件应该是设置自启动的,然而我这里并不需要。但若直接启动app.js是启动不了的,因为在www文件里面,设置了端口是3000,他是通过www文件来启动监听的。 解决办法: 在app.js里面,在最后一行代码之前,添加: app.listen(80); 于是,便可以通过app.js来启动服务器了。 访问效果如图: 假如如果像我一样倒霉,无法用express命令,打开npm也特别慢,可以先找个系统,将这些文件下载好,然后将这些文件复制到不可以用express命令的linux系统下面。 或者看最后的终极解决方案 ps: 如果npm很慢的话,可以考虑装cnpm npm install -g cnpm --registry=https://registry.npm.taobao.org 然后在npm install这一步,使用cnpm install来替代 ————————分割线———————— ④超级终极解决方案: 我传一个装好express的压缩包,直接解压缩后就可以用。 链接: http://download.csdn.net/detail/qq20004604/9587054 ⑤另一个启动方法 即不通过app.js来启动; 在之前④的基础上,打开app.js,删除 app.listen(80); 打开package.json,将 "start":"node ./app.js" 改回 start":"node ./bin/www" 然后 cd bin vi www 将 var port =normalizePort(process.env.PORT||'3000'); 修改为 var port =normalizePort(process.env.PORT||'80'); 然后 cd .. npm start 启动成功,可以直接访问地址来访问页面 【6】把启动的app.js放在后台运行,并且在操作端解除控制后依然可以运行 原本使用npm start的地方,输入 nohup npm start& 即可 【7】app.js之解释 app.js可以说是一切的根本,因此请看注释,解释了每行代码的作用: var express = require('express'); //这个模块在node_modules文件夹中 var path = require('path'); //nodejs自带的,路径相关 var favicon = require('serve-favicon'); //node_modules文件夹中 var logger = require('morgan'); //日志相关,node_modules文件夹中,具体看第19行代码 var cookieParser = require('cookie-parser');//node_modules文件夹中,用来解析cookie的,被express所调用 var bodyParser = require('body-parser'); //用于解析请求体的(应该),被express所调用 var routes = require('./routes/index'); //这里是默认的路由,路径是routes/index.js var users = require('./routes/users'); //这里是默认的路由,路径是routes/users.js //关于路由的更多内容看下面的app.use('/', routes);和app.use('/users', users);的注释 var app = express(); //生成一个express的实例 //设置模板引擎 app.set('views', path.join(__dirname, 'views')); //__dirname表示绝对路径 //console.log(__dirname) //可以取消这一行注释,然后看看__dirname的效果 app.set('view engine', 'jade'); //表示express是使用jade格式的模板的 //下面这行代码是设置左上角的小图标的,将其命名为favicon.ico并放在public文件夹下,如果有的话,取消这行注释 //app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); app.use(logger('dev')); //设置日志显示,如果不需要的话可以注释掉这行代码,建议练手的时候不要注释掉 //可替换参数有default,combined,common,short,tiny,dev(相对tiny有染色)。可以自己试试效果,默认是dev app.use(bodyParser.json()); //解析客户端的请求,通常是通过post发送的内容 app.use(bodyParser.urlencoded({extended: false})); //另一种解析方式,具体不太明白(如果上面解析失败的话会通过这个来) app.use(cookieParser()); //cookies的解析 app.use(express.static(path.join(__dirname, 'public'))); //普通静态html文件、js文件、css文件,都放在public文件夹下,可以直接通过url来访问 //路由的处理 app.use('/', routes); //假如访问的网址是根目录,例如http://121.41.66.68/,交给routes这个js文件来处理,具体请查看routes app.use('/users', users); //假如访问的是/users这样的路径,那么交给users这个js文件来处理,具体略 //我们最后要对这个路由进行处理,让他按照我们想要的方式来做 //这里对非法路径进行处理的,next表示这是一个中间件(即执行完他之后,还会执行下一个,而不是直接在这里结束了) //如果上面没有静态文件(29行)、没有找到被路由处理的文件(32,33行),就会交给这个来处理。 app.use(function (req, res, next) { var err = new Error('Not Found'); err.status = 404; next(err); //由下面的2个app.use中的一个来处理(一般是第一个,除非第一个被注释) }); //原注释说是会对错误进行加亮处理 //这部分和下面的区别在于,这部分会将错误信息暴露给用户,而下面的不会,因此注释掉这部分 //if (app.get('env') === 'development') { // app.use(function (err, req, res, next) { // res.status(err.status || 500); // res.render('error', { // message: err.message, // error: err // }); // }); //} // production error handler // no stacktraces leaked to user app.use(function (err, req, res, next) { res.status(err.status || 500); res.render('error', { //这里的error指调用views下的error模板 message: err.message, error: {} }); }); //这是我自行添加的,用于显示服务器启动的时间 console.log("Server start at :" + new Date()); //导出app,在./bin/www里会被调用 module.exports = app; 【8】页面关系结构 请忽略文件名的大小写问题(手动微笑) 因此,首先有三个需要我们自定义的路由: ①当访问根/时,输出index; ②当访问/login时,输出login ③当访问/reg时,输出reg 其他可能的路由: ④后续可能添加的页面,暂缺; ⑤不符合要求的页面,输出404(express已经完成) 【9】index页面需要包含的功能: 【10】index的路由: 查看app.js中的代码: app.use('/', routes); 这部分代码已经说明了,当访问根的时候,交给routes来处理; 再次查看导入routes的地方: var routes = require('./routes/index'); 说明负责这部分的文件是routes文件夹的index文件 这时打开index.js, router.get('/', function(req, res, next) { res.render('index', { title: 'Express' }); }); 这部分代码,表示当访问根的 ’/’ 页面时,由index这个引擎模板来处理,模板中的title变量,其值为Express。 ———————————————————————— 代码说明: router.get('/' 以上这段代码是在路由基础上进行二段捕获url的,举例: ①假如在app.js里, app.use('/', 然后在路由的处理里,是 outer.get('/' 那么最终针对的url是/ ②假如在app.js里, app.use('/test', 然后在路由的处理里,是 outer.get('/' 那么最终针对的url是/test ③假如在app.js里, app.use('/, 然后在路由的处理里,是 outer.get('/test' 那么最终针对的url依然是/test ④假如在app.js里, app.use('/testA, 然后在路由的处理里,是 outer.get('/testB' 那么最终针对的url是/testA/testB ⑤捕获优先度说明: 首先根据app.js中的app.use出现顺序决定,假如②和③同时出现,那么看②和③在app.js中的代码谁先出现,就交给谁来处理; 再不调用next()方法的情况下,只会被最先出现的那个处理; 如果第一个调用了next()方法,那么第一个处理完后会执行第二个的 ———————————————————————— index.js代码说明: var express = require('express'); //调用express var router = express.Router(); //生成express的Router方法的一个实例 //处理函数 router.get('/', function (req, res, next) { //捕获根url res.render('index', {title: 'Express'}); //res.render方法渲染一个引擎模板, //第二个参数是一个对象,对象里的变量可以在引擎中使用, //第三个参数是回调函数,提供两个参数,分别是err和html,err是错误,html是渲染后的页面。如果使用这个回调函数,那么将不会自动响应,即要用户自己写返回html的命令 }); module.exports = router; 注意,被引擎渲染的文件,默认是在myblog/views文件夹下 【11】使用Bootstrap界面 让我们自己设计界面不是不可以,不过太麻烦,因此我和原书保持一致,使用Twitter Bootstrap风格的页面。 下载他的压缩包文件,并解压缩他, 将css文件放在项目myblog/public/stylesheets下; 将img文件夹直接复制粘贴在myblog/public下; 将js文件放在myblog/public/javascripts下, 我的Bootstrap的版本是v2.3.2 另外,下载jquery,命名为jq.js,放在myblog/public/javascripts下 【12】index.jade说明 下来我们就要修改jade文件了,如果不知道怎么使用的话,可以看我的博客: http://blog.csdn.net/qq20004604/article/details/51773574 extends layout block content h1= title p Welcome to #{title} 第一行的extends layout表示他导入的layout模板(即layout.jade) block content表示以下这部分将套入layout的content位置 内容略。 这时候再过去看layout.jade doctype html html head title= title link(rel='stylesheet', href='/stylesheets/style.css') body block content 他导入的是style.css这个样式表,页面的title是变量title 上面的index.jade中的内容将导入这里的block content区域(因为变量名一样,会对应替换)。 其结构大概如下: 然后我们根据自己实际需求来改造: 直接给出代码: 首先在stylesheets文件夹下创建一个blog.css文件,内里样式为: body { padding-top: 60px; padding-botom: 40px; } #textarea { resize: none; width: 300px; height: 100px; cursor: text; } #postBlog { position: relative; left: 20px; vertical-align: top; } #clearBlog { position: relative; left: -90px; top: 27px; width: 110px; height: 44px; } .myalert { position: absolute; } .displayNONE { display: none; } #scrollToFoot { border: 1px solid #ccc; text-align: center; font-size: 18px; padding: 20px 0; } fotter p { margin-top: 40px; padding-top: 20px; border-top: 1px solid #aaa; font-size: 18px; } .row { color: #555; } 其次是layout.jade doctype html html head title MyBlog By qq20004604 link(rel='stylesheet', href='./stylesheets/bootstrap.min.css') link(rel='stylesheet', href='./stylesheets/bootstrap-responsive.min.css') link(rel='stylesheet', href='./stylesheets/blog.css') script(type="text/javascript",src='javascripts/jq.js') script(type="text/javascript",src='javascripts/bootstrap.min.js') script(type="text/javascript",src='javascripts/blog.js') body div.navbar.navbar-fixed-top div.navbar-inner div.container a.btn.btn-navbar(data-toggle="collapse",data-target=".nav-collapse") span.icon-bar span.icon-bar span.icon-bar a.brand(href="/") MyBlog div.nav-collapse ul.nav li a(href="/") 首页 li a(href="/logout") 登出 li a(href="/login") 登入 li a(href="/reg") 注册 li a(href="/language") 切换语言 div#contrainer.container block content hr fotter p 作者:王冬 QQ:20004604 效果如图: 然后是index.jade: extends layout block content div.hero-unit h1 我的博客 p 这个是基于Nodejs作为后台,jade作为模板来,使用了Express作为框架 br br //这部分暂时用1替代,后续会被更新 if(1) br br a.btn.btn-primary.btn-large(href="/login") 登录 a.btn.btn-large(href="/reg") 立即注册 else textarea#textarea.uneditable-input button#postBlog.btn.btn-large 提交微博 button#clearBlog.btn.btn-large 清空 div#submitError.alert.alert-error.displayNONE.myalert div#submitSuccess.alert.alert-success.displayNONE div.row.content div.span4 h2 烟雨江南说 p 当欲望没有了枷锁,就没有了向前的路 p 只有转左,或者向右 p 左边是地狱,右边也是地狱 div.span4 h2 烟雨江南说 p 那些都是极好极好的 p 可是我偏偏不喜欢 div.span4 h2 烟雨江南说 p 我不怕傻 p 只怕 p 遇不到 p 可以让我变傻的人 div.span4 h2 烟雨江南说 p 人在年轻的时候总会有些莫名的坚持, p 并且以此感动着自己, p 却时常会在不经意间让真正重要的东西从指间流走。 div.span4 h2 烟雨江南说 p 记忆真是一种奇怪的东西, p 有时候会涤荡所有的苦难,只留下温情, p 有时候却磨灭掉曾有的欢乐,唯剩下苍白和丑陋。 div.span4 h2 烟雨江南说 p 那存在的,都是幻影。 p 那永恒的,终将毁灭。 p 世界万物,缤纷色彩,都是被蒙蔽的人心罢了。 div.span4 h2 烟雨江南说 p 诸神以真相示人,而世人却视而不见 div.span4 h2 烟雨江南说 p 只有绵羊会向狮子要求平等, p 而狮子们从来不会这样想。 div.span4 h2 烟雨江南说 p 愿迷途的旅人,从此得享安息。 p 因理想而不朽,因归返而救赎。 div#scrollToFoot 滚动到底部然后加载内容 效果如图: 但此时,上面的页面切换目前还都是无效状态; 滚动然后加载内容,也正处于无效状态; 注册和登录按钮,点击后也无法正常跳转; 我们需要依次解决这些问题。 【13】登录页面 接下来我们添加登录页面的路由和模板。 路由,打开app.js,在 app.use('/', routes); app.use('/users', users); 之前添加 var reg = require('./routes/reg'); var login = require('./routes/login'); 之后添加: app.use('/reg', reg); //注册的,reg.js来处理 app.use('/login', login); //登录的,login来处理 这样的话,就添加了注册和登录页面的路由了,但目前,我们还缺其具体文件和代码。 进入routes文件夹,创建reg.js和login.js reg.js var express = require('express'); //调用express模块 var router = express.Router(); //调用模块的Router方法 router.get('/', function (req, res, next) { res.render('reg') }); module.exports = router; login.js var express = require('express'); //调用express模块 var router = express.Router(); //调用模块的Router方法 router.get('/', function (req, res, next) { res.render('login') }); module.exports = router; 现在又缺模板文件了。 进入views文件夹,创建reg.jade和login.jade reg.jade extends layout block content form.form-horizontal(method="post") fieldset legend 注册 div.control-group label.control-label(for="username") 用户名 div.controls input.input-xlarge#username(type="text",name="username") p.help-block 你的账户名称,用于登录和提示 div.control-group label.control-label(for="password") 口令 div.controls input.input-xlarge#password(type="password",name="password") div.control-group label.control-label(for="password-repeat") 重复输入口令 div.controls input.input-xlarge#password-repeat(type="password",name="password-repeat") div.form-actions button.btn.btn-priamry(type="submit") 注册 login.jade extends layout block content form.form-horizontal(method='post') fieldset legend 登录 div.control-group label.control-label(for='username') 用户名 div.controls input.input-xlarge#username(name='username',type='text') div.control-group label.control-label(for='password') 密码 div.controls input.input-xlarge#password(name='password',type='password') div.form-actions button.btn.btn-primary(type="submit") 登录 这个时候,注册和登录页面已经可以访问了(虽然还没有添加逻辑)
操作系统: CentOS 6.5 64位,用的阿里云的ECS里最便宜的(但还是好贵啊!) 【0】下载 https://nodejs.org/en/download/ nodejs的官网, 我下的是64位。 文件的上传:上传到服务器的话,我是用ftp,对于我这种新手来说,用ftp来处理文件的转移/复制/粘贴最方便了。 如果是虚拟机的话,我不太清楚,反正感觉linux下载文件挺麻烦的。。。 不过或许可以用图形化的linux操作系统,然后用浏览器下载? 【1】.tar.xz解压缩方法 xz压缩文件方法或命令 xz -z 要压缩的文件 如果要保留被压缩的文件加上参数 -k ,如果要设置压缩率加入参数-0 到 -9调节压缩率。如果不设置,默认压缩等级是6. xz解压文件方法或命令 xz -d 要解压的文件 同样使用 -k 参数来保留被解压缩的文件。 创建或解压tar.xz文件的方法 习惯了 tar czvf 或tar xzvf 的人可能碰到tar.xz也会想用单一命令搞定解压或压缩。其实不行tar里面没有征对xz格式的参数比如z是针对 gzip,j是针对bzip2。 创建tar.xz文件:只要先tar cvf xxx.tar xxx/ 这样创建xxx.tar文件先,然后使用xz -z xxx.tar 来将xxx.tar压缩成为xxx.tar.xz 解压tar.xz文件:先xz -d xxx.tar.xz 将xxx.tar.xz解压成xxx.tar 然后,再用tar xvf xxx.tar来解包。 【2】.tar解压缩方法 tar xvf filename.tar 【3】判断自己下载的文件,然后编译 如果跟我下的是一样,那么下载的是编译好的文件, 如何判断? 简单说就是解压后,在bin文件夹中已经存在node以及npm,如果你进入到对应文件的中执行命令行一点问题都没有,不过不是全局的,所以将这个设置为全局就好了。 cd node-v0.10.28-linux-x64/bin ls ./node -v 注: ①第一行命令的node-v0.10.28-linux-x64就是你看到的nodejs解压缩后的那个文件夹(可能有所不同)例如我的文件夹名字是node-v4.4.7-linux-x64。 ②第三行命令是查看nodejs的版本,如果能成功查看说明没问题 这就妥妥的了,node文件夹具体放在哪,叫什么名字随你怎么定。 例如我的nodejs解压缩后的文件夹是/usr/nodejs4.4.7/node-v4.4.7-linux-x64 然后设置全局: ln -s /usr/nodejs4.4.7/node-v4.4.7-linux-x64/bin/node /usr/local/bin/node ln -s /usr/nodejs4.4.7/node-v4.4.7-linux-x64/bin/npm /usr/local/bin/npm 这里/usr/nodejs4.4.7/node-v4.4.7-linux-x64这个路径是你自己放的,你将node文件解压到哪里就是哪里。(也就是你在上面看到的node文件和npm文件夹) 注: ①如果正常的话,应该是下图这样,可以通过输入node来成功运行代码的 ②如果不是编译好的版本,我给个参考链接: http://www.xitongzhijia.net/xtjc/20150202/36680.html —————————————————— 以下似乎是处理未编译的源代码的方法(按照我的方法可以无视到分割线结束) 【4】CentOS更新yum yum update 【5】安装g++ 如果你使用 Linux,那么你需要使用g++ 来编译 Node.js。 在Debian/Ubuntu中,你可以通过 apt-get install g++ 命令安装g++。 在Fedora/Redhat/CentOS中,你可以使用 yum install gcc-c++安装。 【6】另一个工具 libssl-dev 是调用 OpenSSL 编译所需的头文件,用于提供 SSL/TLS 加密支持。Mac OSX 的 Xcode 内置了 libssl-dev。 在 Debian/Ubuntu 中,你可以通过 apt-get install libssl-dev 命令安装。 在 Fedora/Redhat/CentOS 中, 你可以通过 yum install openssl-devel 命令安装。 同样,你也可以访问 http://openssl.org/ 下载一个。 【7】 接下来,进入 Node.js 源代码所在目录,运行: ./configure make sudo make install 18 第2 章 安装和配置Node.js 之后大约等待20分钟,Node.js 就安装完成了,而且附带安装了 npm。 如果你使用 Mac OS X,还可以尝试使用 homebrew 编译安装 Node.js。 首先在 http://mxcl.github.com/homebrew/获取 homebrew,然后通过以下命令即可自动解析编译依赖并安装Node.js: brew install node —————————————————— 【8】测试下能否访问: 输出node,进入nodejs的命令行模式, 然后复制以下代码到linux(可以直接全部复制粘贴); var http = require("http"); var url = require("url"); function onRequest(request, response) { var pathname = url.parse(request.url).pathname; console.log("Request for " + pathname + " recived."); response.writeHead(200, {"Content-type": "text/plain"}); response.write("Hello word!"); response.end(); } http.createServer(onRequest).listen(80); console.log("Server has started!"); 如果是本机的话,访问127.0.0.1; 如果是服务器的话,访问服务器的ip地址 正常的话,你会看到如下页面: 退出: 按Ctrl+d可以退出nodejs,退出后,页面即无法访问了
(37)只需要前端知识,就能理解的ajax教程 ①新人学web前端时,遇见的第一个困难就是ajax 这里对于以下问题,不深入,只用 最简单的话 讲清楚什么是ajax,并且 让你会用ajax。 新人常见问题: 【1】到底什么是ajax? 【2】ajax时发生了什么事情? 【3】为什么ajax时要有url? 【4】为什么ajax时,有时候会有一个对象(object),有时候没有,有时候url后面有?或者&或者=或者其他什么? 【5】我怎么知道服务器(后端)返回什么内容?我如何处理? ②ajax是什么? 用通俗的话一步一步来解释: 【1】我决定去你家拜访,首先得知道你家在哪吧。你家的地址就是http的url; 【2】我到你家来做客,这是进行http访问(访问你家); 【3】你给我端茶倒水,这是你给我返回了一些资源,通常是一个网页(html); 【4】你在你家客厅摆放了一些吃的,并且我知道这些东西我可以吃。我知道可以吃,然后我就告诉你我想吃点东西,这是我再次发起请求; 【5】你让我不用动,然后给我拿了一点糕点过来。此时,我依然坐在你家的客厅沙发上,我只告诉你我要吃东西,你就给我了。 【4】和【5】这两步结合起来,这就是ajax; ajax的实质是,在原有页面的基础上,发生某个js事件,然后执行了一个请求,服务器给予了一个回应。 如图: 如果说的再简单一点,就是我(网页)问服务器(后端)要东西,然后服务器给我了。 至于怎么要,下面说。 ③ajax时发生了什么事情? 首先请看上图。 【1】触发某事件,实际上这里是指触发ajax事件。例如jquery里get请求事件: $.get(URL,callback); 或者是一个post请求事件 $.post(URL,data,callback); 【2】发起请求后,就是执行以上事件。 这个事件必须的参数是URL,可选参数是data(get没有)和callback(回调函数,请求完成后执行的); 关键是URL 理解URL: 大部分新人之所以不明白ajax,主要是不明白这个url。 按照普通人的理解,url就是一个地址,比如一个文件a.txt他的url可以是C:\a.txt 当我访问c:\a.txt时,就访问的是这个文本文件。 这样理解当然也不算错误,但不全面。 按照我的解释,你可以把url理解为一个暗号,和服务器(后端)预先约定好的暗号。 注意,这里的url,通常指的是例如: https://www.zhihu.com/topic/19626558/hot 中的 /topic/19626558/hot 为什么这么说呢?很简单。 你发送的url,事实上在后端看来,只是一个字符串,在未进行特殊处理的情况下,他并不能直接访问文件。 就比如上面我写的那个知乎的url,可能存在一个文件名是hot这样的文件么?显然并不可能。 事实上,不存在一个根目录下的topic文件夹,也不存在一个名为19626558的文件夹。 但为什么时候我们可以通过访问这个url来访问一个网页呢?事实上,是因为在http服务器进行处理过的了。 这里我简单的说一下其流程: 【1】我们前端和后端约定了一个url(暗号),这个暗号以'/topic'开头,但这个暗号表示什么意思呢? 【2】当后端遇见一个以这样暗号为开头的,他就知道,我是在访问一个话题类的页面, 但目前为止,后端还不知道话题是什么,自然也没法给我们返回内容; 如果我们不是以/topic为开头,而且以其他字符串为开头,自然也会有对应的代码来处理我们的请求(包括无效的字符串,也是有办法处理的) 【3】然后我们继续发送了下一部分'/19626558',这个实际上就是话题的id。 当后端获取到这个话题的id后,他会去数据库里进行查询。 如果这是一个有效的话题id,那么会返回给我们一大堆跟这个id相关的内容; 假如后端发现这个id是无效的,那么会告诉我们 注意这个页面是预制好的。 【5】然后后端会查看url(暗号)的下一部分'/hot '。服务器发现是'/hot',于是他按照热门程度,将内容排序好后返回给我们,避免我们看到的都是没人回复的帖子(那也太浪费感情了)。 如果下一部分是'/newest '呢,那就以时间发表顺序来返回内容给我们(毕竟有时候我们只想看看最新的); 那么假如什么都没有呢?是个空字符串的话,该怎么办?后端也有预先准备的办法,那就是默认给我们发送'/hot'的内容,并且将我们的url设置为'/hot'结尾的。 如果是其他结尾的,那么就可能返回一个404的页面(就像上面那样) 【6】按道理来说,讲解就到这里结束了。但是为了防止理解的过于片面,我假设一种情况。 比如说有一个调皮的程序员,他写了这么一段代码,假如你访问的url是一个/topic/19626558/joke(显然知乎并不会这么做), 那么他会将你转到一个小游戏的页面(他显然跟topic毫无关系,跟我们要访问的url也毫无关系)。 因此要搞清楚的是,url,只是我们约定好的一种形式,我通常会照着这个通俗的做法去做,但是我也可以任性的做些别的,比如/a/b/c页面却是/a/b页面的父页面(通俗做法显然是相反的);/a/b页面和/c页面是平级页面。 所以我说与其将url理解为一个地址,不如理解为一个暗号,一个你与后端程序员约定好的暗号。 你告诉他,当我说这个暗号的时候,我可能给你发送些什么东西(比如id,密码之类),你要给我返回些什么东西,比如一个页面、一个表格、一段动画这样。 ④为什么要有url? 就像上面说的那样,他是一个暗号,你们必须有同样的暗号时,他才知道你想做什么; 例如/game时是想访问游戏页面,/news时是想访问新闻页面,/login是想登陆,/logout是想登出。 至于究竟是想访问页面呢,还是想登陆登出呢,或者是想获取一段数据呢?这要根据url(约定的暗号)来决定。 ⑤为什么ajax时,有时候会有一个对象(object),有时候没有,有时候url后面有?或者&或者=或者其他什么? 这个问题事实上就涉及到了http请求的方法了。 我们通常用的http请求方法有两种,分别是get和post。 他们的区别简单的来说,就是一个会附带一个对象(post),另外一个是不带的(get)。 先说get和post的区别: 【1】get,如字面意思,请求,获取。 我(前端)告诉你(后端),我想获取什么东西(根据暗号事先约定好的),你当我说这个暗号(发送http请求)的时候,就返回给我; 【2】post,如字面意思,邮寄、发送。 假如我在玩游戏,我要下线/退出了,因此我要存档的对吧。你可以把存档这个行为,理解为post。我存档时,肯定要把我的经验、等级、装备、任务进度之类之类的内容,存到文件中去(在后端,一般是存到数据库之中); 但后端怎么知道我的这些数据呢?一般就是通过post了,例如我发送这样一个对象: {lv:100, name:"水哥", exp:987654321,atk:12345, def:12345, gold:10000} 后端就会获取到这个对象,然后他就会把这个对象解析出来,存到数据库之中(比如MySQL)。 get一般在无敏感数据、数据量小的时候使用; post一般在有敏感数据(比如密码),数据量大的时候使用; 因此,“为什么有时候会有这个对象”,这个问题就解释清楚啦。 另外一个,url里为什么有时候会有很长串的字符呢? 比如说: /friend?id=123&online=true#abc 我将其分为三部分: 【1】问号以前的:/friend 【2】#号以前的:id=123&online=true 【3】#号以后的:abc 第一部分属于url,即我们约定的暗号,称为pathname 第二部分和第三部分都是请求部分,但他们有所区别: 仔细看第二部分,他们以&分隔,分别为id=123与online=true,这是典型的KV结构(即key和value)。而&符号通常表示and。因此我们可以将其理解为一个对象:{id:123,online:true} ——具体怎么转换是后端处理的事情,前端只需要这么理解就可以了。 第三部分是一个字符串abc,我们将其称为哈希地址。 他们有什么用呢?很简单。 【1】假设我在玩网页游戏,我需要获取在线的好友列表,因此以/friend开头,表示我要获取好友列表。 【2】假设我的id是123,因此我发送id=123(表示要找我的好友),但我的好友里面有在线的,也有非在线的,服务器怎么知道我要获取哪种呢?那么就通过检查online的值,发现其值为true,于是就知道我找的是在线好友了。 【3】另外还有一个哈希地址。我可以这么解释,我需要查看的是好友名称为abc的人的资料。因此,服务器返回的是一个在线的好友列表,以及好友名称为abc的人的全部资料。 (ps:具体怎么解释,全看约定) ⑥我怎么知道服务器(后端)返回什么内容?我如何处理? 还记不记得之前jquery的两个方法,他们都有一个回调函数? 这个回调函数的参数呢,就是返回的内容。 例如: $.get('/abc', function(item){ //do something }) 假设后端返回一个这样的对象: { id:1, name:"abc"} 那么回调函数的参数item的值,就是{ id:1,name:"abc"} 至于怎么知道的,想要深入的话可以去看源代码,只是了解ajax的话,知道这个事实就行了。 那么这个函数什么时候执行呢? 是在服务器返回内容之后,浏览器接受到的时候执行的。 Notes: 【1】假设我在0s的时候发送了这个get请求, 【2】在1s的时候,服务器接受到这个请求,他用了0.1s来处理,在1.1s的时候,给我返回以上那个对象; 【3】又过了0.5s,我接受到了这个返回对象(此时是1.6s),因此这个回调函数执行的时候,是在我发送get请求后1.6s执行的。 为什么要特别强调这个过程,原因在于: 当我们用ajax来给一个变量赋值时,我们不能在ajax外对赋值结束后的这个变量进行处理。 例如: var id = null; $.get('/abc', function(item){ id = item.id; }) console.log(id); 你觉得console.log出来的id的值是什么? 答案:是null 原因在于,console.log(id)这段代码,在上面第【1】步执行结束后执行的。因此他的执行时间,实际上可能是第0.01s(第0s发送的get请求) 而等他获取值的时候,已经在第1.6s了,因此,第0.01时他的值是初始值(null) 这是新人很容易犯的一个错误,务必提醒自己,多多注意。 ⑦补充: 对于ajax,纯前端是无法使用的,因为ajax的实质就是前后端交互,建议大家看一看我的Nodejs的博客,架设一个最简单的Nodejs服务器(非常容易),来进行一次简单的前后端交互,对于提高自己的ajax水平会有很大的帮助。
(38)动态视图助手 express版本:4.13.4 ①作用:假如我们需要一个变量,在不同地方的模板(jade文件)都需要调用。 显然,我们不应该使用全局变量(因为可能会带来污染); 事实上,我们需要的是仅仅在模板中起作用的变量,因此视图助手的作用就在这里了。 ②流程: 【1】首先,调用express模块,就像我们之前做的那样(事实上,不需要额外声明,当我们使用express框架的时候自然会调用它); 【2】按照正常情况,我们需要进行路由处理,当使用视图助手时,有一件很重要的事情,就是在进行任何路由处理之前使用视图助手。 原因在于,视图助手的处理和路由处理是类似的,他会拦截req和res请求,如果先进行路由的话,那么视图助手是不会被执行的。(即使被执行了,那么由于进行路由处理的时候,没有获取到视图助手的变量,也是不会达成预期目的的); 【3】当明确我们视图助手声明的位置后,我们开始写我们的视图助手: 注意,这里是动态的视图助手; var app = express(); //生成一个导入的express模块的实例 //定义一个动态视图助手,这段代码要放在路由之前 app.use(function (req, res, next) { //这个是示例,定义一个动态视图助手变量 res.locals.testStr = "这是一个动态视图助手变量"; //这是示例,定义一个动态视图助手方法 res.locals.testFun = function () { return "这是一动态视图助手——函数的返回值" }; //这是另一种定义的方法这种方法似乎不行 /* res.locals({ testStr2: "动态变量2", testFun2: function () { return "动态函数2" } })*/ //必须有next,不然后续的路由没法执行了(因为只被执行一次) next(); }) 注意:上面的方法二,据说是可以的,但我自己验证后是不行的,如果可以的话,欢迎指正 写法很简单,app.use方法,参数是回调函数; 回调函数的参数分别是req(请求),res(回应),next(下个函数); 然后把变量放在res.locals之中,声明一个变量testStr,给他赋一个值。 当我们在模板里调用这个变量时,直接使用testStr就可以。 同理,也可以声明一个函数testFun,调用时使用testFun()来获取其返回值(当然,可能在这个函数里对testStr进行修改)。 【4】按照我找的教程,下面被注释掉的方法应该也可以,但事实上,我自己测试时,他会提示res.locals is not a function,也就是说,不能正常使用。因此我注释掉了。 【5】最后调用next(),正常执行路由(否则后面路由的代码不会被执行)。 【6】当执行render方法时,正常执行,但是在模板里,可以调用对应的变量。如果想在render里使用变量的话,那么需要这么写: router.get('/', function (req, res, next) { //req是请求,res是回应 res.render('index', { title: res.locals.testStr }); }); 【7】如果在jade模板里调用的话,只需要这么写就可以了: h1 #{title}
(84)摧毁一个widget ①假如我们想摧毁一个widget该怎么办? ②创建时,将该widget赋值给一个变量,通过调用该变量的destroy方法,可以直接摧毁;——成功 ③假如我们将该widget挂载到一个dom结点下,那么摧毁这个dom(例如domConstruct.empty(该dom结点),只能让我们找不到这个widget,但实际上这个widget还是存在的; 验证方法:该widget在创建的时候添加一个定时器函数,用于定时console.log自身,从widget创建开始,该定时器会通报,摧毁其父dom结点后,依然会通报该widget(和未摧毁前没有什么不同)。而如果正常摧毁的话,其内部一些元素将为空。——失败 解决办法: 通过aspect或者topic/subscribe,来将父容器摧毁dom这个事件,和widget的destroy方法绑定起来。 当父容器调用其自身方法摧毁dom时,子widget可以监听到,然后destroy自身。——成功 ④假如能获知该widget的id的话,通过调用"dijit/registry"模块,使用其方法registry.byId(widget的id),来获取该widget,然后调用其destroy方法摧毁该widget即可。——成功
(35)查看对象是否有某个属性(转) 来源: http://www.cnblogs.com/snandy/archive/2011/03/04/1970162.html 内容: 两种方式,但稍有区别 1,in 运算符 1 2 3 var obj = {name:'jack'}; alert('name' in obj); // --> true alert('toString' in obj); // --> true 可看到无论是name,还是原形链上的toString,都能检测到返回true。 2,hasOwnProperty 方法 1 2 3 var obj = {name:'jack'}; obj.hasOwnProperty('name'); // --> true obj.hasOwnProperty('toString'); // --> false 原型链上继承过来的属性无法通过hasOwnProperty检测到,返回false。 需注意的是,虽然in能检测到原型链的属性,但for in通常却不行。 当然重写原型后for in在IE9/Firefox/Safari/Chrome/Opera下是可见的。见:forin的缺陷 (36)合并两个数组 ①将一个对象放到数组里,是arr.push(obj) ②将两个数组合并起来是: var arr3 = arr1.concat(arr2); 返回值是合并好的数组
①情景: 有一父容器div,其高和宽不定,称之为P; 该父容器有两个子div,左右布局,左定宽,满高,右自适应剩余区域; 其中,定宽称之为A,变宽称之为B; A和B是等高的; P必然能容纳A和B 可能的附加条件: 【1】A和B的宽高度可能不会撑满P,即上下左右都可能留有空隙,但这些空隙的宽或者高是已知的; 【2】A和B之间可能有一定间隙; 备注: 【1】左自适应右定宽方法同理; 【2】上下布局同理; ②方法: 原理:利用P的padding属性和A的margin属性来布局; 【1】由于P宽高不定,因此可以忽视对P的宽高设置; 【2】假设A的宽度为100px;A距离左侧10px,距离B有10px。因此,B的左侧实际有120px宽度;先设置P的css属性如下设置: .P { padding-left: 120px; width: 略; height: 略; } 【3】设置A的CSS属性,如果要加border属性,那么需要注意A和B的box-sizing要设置为border-box .A{ margin-left: -110px; width: 100px; box-sizing: borer-box; float:left; } Note: 《1》如果有border属性,那么content区域要对应减少border的宽度(并且如果两侧都有,那么是双倍的宽度); 《2》必须添加float属性,以使其脱离文档流;假如抛弃float属性,而采用display:inline-block属性的话,会导致右侧的B会靠近A,而不是在我们预想的区域之中。 【4】设置B的CSS属性,没有什么特殊的,只需要设置宽高为100%即可; .B{ width: 100%; height: 100%; box-sizing: border-box; } ③如此,便能实现左定宽,右变宽的布局了;并且由于没有使用CSS3属性,并且margin的范围没有超出P的盒模型,因此相对兼容性也很好。 ④可能存在的问题: 【1】由于使用了float属性,也许在某些版本的浏览器中可能出现问题(真有这种可能?); 解决办法: 取消A的float属性,用以下CSS替代: display: inline-block; ——A和B都设置 vertical-align: top; ——A和B都设置 margin-right: 若干px; ——A设置,注意:这行的值可能并非10px,也许只有5px 【2】A的内部文字,在右侧可能溢出的问题: 解决办法:对A设置padding-right属性
(35)express框架的send方法 ①send方法用的还挺多的,因此需要明确其作用; ②原型是: res.send([body|status], [body]) 即既可以直接发送内容,也可以第一个参数状态,第二个参数内容。 如果直接发送内容的话,状态会被自动补全; ③发送的内容: 示例: res.send(newBuffer('whoop')); res.send({ some: 'json' }); res.send('some html'); res.send(404, 'Sorry, we cannot find that!'); res.send(500, { error: 'something blew up' }); res.send(200); 【1】第一种是发送二进制(binary)内容,当其参数为Buffer(缓冲)时,Content-Type 会被设置为 "application/octet-stream" ,而这个表示其文件后缀(文件类型)是某些类型,具体可以查看: http://www.w3school.com.cn/media/media_mimeref.asp 而wiki上是这么说的 · application/octet-stream(任意的二进制数据) 也就是说这是一个任意的二进制数据,具体如何解释要看实际情况(比如后缀名),比如他可能是一个img,也可能是一个video。 【2】假如发送字符串,那么将被解释为html文件; 也就是说,Content-Type 默认设置为"text/html": 例如,发送了一个post,然后我res.send("aaa"),那么网页将跳转到一个只有文本aaa的页面; 【3】假如参数为Array(数组),或者Ojbect(对象),那么将返回一个JSON;
(34)用连接池来控制mysql(入门版) ①第一步,创建一个连接池:(和之前普通创建mysql的连接对象很像) var mysql = require("mysql"); var pool = mysql.createPool({ host: '127.0.0.1', user: 'root', password: '', port: '3306', database: 'test' }) 我们是将其单独放在一个文件,这样的话,如果以后修改mysql的连接属性(比如地址、用户名、密码、端口或者表),就不用满世界找这些东西了,只需要修改这个文件即可。 ②第二步,用连接池来连接: 在连接之前,需要说明的是,之前的方法,是三步:连接(connect)、请求(query)、结束(end)。 在使用连接池的时候,也是三步:连接(getConnection),请求(query)、断开连接(release)。但区别在于,query和release可以写在getConnection这个函数里面。 具体如代码: var db = {}; db.con = function (callback) { //callback是回调函数,连接建立后的connection作为其参数 pool.getConnection(function (err, connection) { console.log("connect start...") if (err) { //对异常进行处理 throw err; //抛出异常 } else { callback(connection); //如果正常的话,执行回调函数(即请求) } connection.release(); //释放连接 console.log("connect end...") }) } 第一步是创建一个空对象db,为了封装,我们把代码都放在db这个对象之中,然后导出的时候只导出db,这样可以避免对连接池属性进行误操作。 第二步是创建一个函数con作为db这个对象的一个方法,然后回调函数作为参数传递给这个con方法,也就是说,当我调用函数时,实际上是这么写的: db.con(function (connect) { 函数内容暂略 }) 这个作为参数的函数,将在con这个函数的内部被执行。 如果不太明白的话,稍后再说; 在con函数里,执行了pool.getConnection这行代码,其作用是连接mysql数据库。 他有一个回调函数,这个回调函数有两个参数, 第一个参数是err,如果出错的话,err将有值,应该对其进行处理; 第二个参数是connection,他实际上就是指连接成功后的对象,用其进行query处理,就像这样: connection.query(mysql的语句, 可能的参数, 回调函数); 然后也用其进行释放连接: connection.release(); console.log("connect start...") 回调函数的第一行代码是consoloe.log,通报连接开始; console.log("connect end...") 最后一行代码通报连接结束(事实上连接可能还没有结束,因为他只是getConnection的回调函数,在其本身内部可能还有一些处理,但我们进行的处理已经结束了); if (err) { //对异常进行处理 throw err; //抛出异常 } else { callback(connection); //如果正常的话,执行回调函数(即请求) } connection.release(); //释放连接 这里,分别是: 【1】对异常进行处理; 【2】正常情况下的处理; 【3】释放连接; 正常的处理,将连接作为参数,传递给上面mysql.con函数的参数,作为mysql.con的参数的参数进行处理。因此实际上其流程是这样的: 函数被调用(这里指con这个自定义的函数) ——》回调函数作为参数传入(con(callback)) ——》回调函数获得connection作为参数,并执行自己callback(connection) ——》回调函数内部调用自己的第一个参数(事实上就是connection)进行请求(connection.query); ③导出封装好的db对象(其只有一个方法,那就是con); module.exports = db; ④假设我们把以上代码放在一个叫做db.js的文件之中。然后我们需要引用他: var db = require('../db') ⑤然后比如我们需要请求一个数据: db.con(function (connect) { connect.query('SELECT * FROM bloguser WHERE username = ?', [username], function (err, result) { if (err) { console.log("select username:" + username + " error, the err information is " + err); return } console.log(result); }) }) 注意,我们所有请求数据的内容,都写在了这个con的回调函数中,由于封装好了,因此我们不关心他如何去连接数据库,如何断开。只需要关心请求这一步即可。 query是其自带的方法,因此我们按要求调用即可; 在请求后,我们依然是分别对err和结果进行处理; 假如err,提示错误,然后返回(这里的返回指的是返回在调用这个db.con的方法的地方); 假如正确,那么console.log结果。 ⑥当这个时候,我们利用连接池来请求mysql数据库的行为就完毕了。 下面附上完全的代码: db.js var mysql = require("mysql"); var pool = mysql.createPool({ host: '127.0.0.1', user: 'root', password: '', port: '3306', database: 'test' }) var db = {}; db.con = function (callback) { //callback是回调函数,连接建立后的connection作为其参数 pool.getConnection(function (err, connection) { console.log("connect start...") if (err) { //对异常进行处理 throw err; //抛出异常 } else { callback(connection); //如果正常的话,执行回调函数(即请求) } connection.release(); //释放连接 console.log("connect end...") }) } module.exports = db; 查询的js文件(需要修改表名,字段名,建议练手时把查询条件写死,能正常运行后再修改): var db = require('../db') db.con(function (connect) { connect.query('SELECT * FROM bloguser WHERE username = ?', [username], function (err, result) { if (err) { console.log("select username:" + username + " error, the err information is " + err); return callback(err); } console.log(result); }) })
先上DEMO, 虽然丑,那是因为零级按钮的界面太丑了,图标也丑┑( ̄Д  ̄)┍ 这两个优化后会好很多,毕竟美观不是我的特长嘛 DEMO链接: http://download.csdn.net/detail/qq20004604/9568685 (83)二级下拉菜单 ①过程描述: 【1】数据来源:一个数组,具体格式为: var dataArr = [{text: "测试1", img: "test"}, {text: "测试2", img: "test"}, { text: "测试3", img: "test", children: [ { text: "测试", img: "test", children: [ {text: "测试", img: "test"}, {text: "测试", img: "test"} ] }, {text: "测试", img: "test"} ] } ] 树形结构; 数组每个单元由text(文字)属性和img(图片名)属性; 假如其有下一级下拉菜单,那么将有children属性(如果没有则无); 因为有两级,所以部分会有两层children属性; 【2】添加形式: 树的最顶层被显式的显示出来,如果其有下拉菜单,则有向下的箭头图标; 一级下拉菜单(第一层children属性里的元素),在点击显式显示的元素后,被显示出来,再次点击任何区域,则隐藏;如果其有下一级下拉菜单,则该行右侧有向右的箭头图标; 二级下拉菜单,在鼠标移动到其父结点时被显示; 效果图如图: (上面的DEMO图) ②代码: 我已经将其整合在一个html文件里,因此直接贴出来全部的,注意图片路径和html文件路径在一起,dojo和jquery文件在html文件的上级目录。具体请查看代码的引用方式。 由于代码较长,建议自行建立一个html文件,然后将全部代码复制进去后查看。 HTML的dom结构参照最后一部分被注释掉的内容(缺少隐藏的逻辑); <html> <head> <style> .parentDiv { height: 40px; background-color: #e8e8e8; line-height: 40px; } .parentDiv .data { background-color: #b8b8b8; color: white; height: 26px; -webkit-border-radius: 2px; -moz-border-radius: 2px; border-radius: 2px; line-height: 26px; padding: 0 5px; margin-left: 7px; display: inline-block; position: relative; top: 7px; cursor: pointer; } .parentDiv .displayNONE { display: none; } .parentDiv .data.focus { background-color: deepskyblue; } .parentDiv .data span.img { display: inline-block; width: 25px; height: 26px; background-position: center; background-repeat: no-repeat; background-size: 16px 16px; } .parentDiv .data span.text { display: inline-block; height: 26px; line-height: 26px; vertical-align: top; font-size: 13px; } .parentDiv .data span.triangle { display: inline-block; width: 10px; height: 26px; background-position: center; background-repeat: no-repeat; background-image: url("triangle_down.png"); } .parentDiv .data .row span.expendlistTriangle { display: inline-block; width: 20px; height: 10px; float: right; position: relative; top: 8px; right: -10px; background-position: center; background-repeat: no-repeat; background-image: url("triangle_right.png"); } .parentDiv .data .row:hover span.expendlistTriangle { background-image: url("triangle_right_hover.png"); } .parentDiv .data .list { z-index: 25; list-style: none; position: absolute; left: 0; width: 200px; top: 40px; color: #7d7d7d; border: 1px solid #b9b9b9; background-color: white; box-shadow: 0px 2px 1px 1px #ddd; border-radius: 10px; } .parentDiv .data .list .before { background-image: url("triangle_top.png"); position: absolute; width: 20px; height: 10px; z-index: 30; top: -10px; background-size: 20px 10px; left: 15px; } .parentDiv .data .expendlist .expendlistbefore { background-image: url("triangle_left.png"); position: absolute; width: 10px; height: 30px; z-index: 30; top: 5px; background-size: 10px 20px; left: -10px; background-repeat: no-repeat; background-position: center center; } .parentDiv .data .list .row { position: relative; display: block; padding: 0 10px; } .parentDiv .data .list .row .img { vertical-align: middle; } .parentDiv .data .list .row:hover { color: white; background-color: #f37b3f; } .parentDiv .data .list .row .expendlist { display: none; position: absolute; top: -5px; } .parentDiv .data .list .row:hover .expendlist { display: inline-block; } .parentDiv .data .list .row .expendlist li { padding: 0 5px; } .parentDiv .data .list .row:hover .expendlist li { color: #7d7d7d; } .parentDiv .data .list .row:hover .expendlist li:hover { color: white; background-color: #f37b3f; } .parentDiv .data .list .row:nth-child(2) { border-radius: 10px 10px 0 0/10px 10px 0 0; } .parentDiv .data .list .row:last-child { border-radius: 0 0 10px 10px/0 0 10px 10px; } .parentDiv .data .expendlist { z-index: 25; position: absolute; left: 105%; list-style: none; width: 100%; border: 1px solid #b9b9b9; background-color: white; box-shadow: 0px 2px 1px 1px #ddd; } </style> <script src="../dojo/dojo.js"></script> <script src="../jq.js"></script> <script> require([ "dojo/dom-construct", "dojo/dom-class", "dojo/dom-style", "dojo/mouse", "dojo/on", "dojo/domReady!" ], function (domConstruct, domClass, domStyle, mouse, on) { var tabArr = []; var dataArr = [{text: "测试1", img: "test"}, {text: "测试2", img: "test"}, { text: "测试3", img: "test", children: [ { text: "测试", img: "test", children: [ {text: "测试", img: "test"}, {text: "测试", img: "test"} ] }, {text: "测试", img: "test"} ] } ] dataArr.forEach(function (item) { tabArr.push(createTab(item)); }) domClass.add(tabArr[0], "focus"); var lastTab; lastTab = tabArr[0]; function createTab(obj) { //创建标签页(就是智能分析那一排) var node = domConstruct.create("div", { class: "data" }, "parentDiv"); var img = domConstruct.create("span", { class: "img", style: "background-image:url(" + obj.img + ".png)" }, node); var text = domConstruct.create("span", { class: "text", innerHTML: obj.text }, node); on(node, "click", function () { domClass.remove(lastTab, "focus"); domClass.add(node, "focus"); lastTab = node; }) if (typeof obj.children === "object") { //如果有children属性,说明有下拉菜单,那么创建它 var text = domConstruct.create("span", { class: "triangle" }, node); createTabList(node, obj.children); } return node; } function createTabList(node, obj) { //创建一级下拉菜单 var list = domConstruct.create("div", { class: "list displayNONE" }, node); domConstruct.create("span", { class: "before", }, list); obj.forEach(function (item) { var row = domConstruct.create("div", { class: "row", innerHTML: item.text }, list) var img = domConstruct.create("span", { class: "img", style: "background-image:url(" + item.img + "_unfocus.png)", }, row, "first"); on(row, mouse.enter, function () { domStyle.set(img, "background-image", "url(" + item.img + ".png)"); }) on(row, mouse.leave, function () { domStyle.set(img, "background-image", "url(" + item.img + "_unfocus.png)"); }) if (typeof item.children === "object") { domConstruct.create("span", { class: "expendlistTriangle" }, row); createExpendList(row, item.children); } }); var evt; on(node, "click", function () { //点击按钮 if (domClass.contains(list, "displayNONE")) { //如果列表隐藏中 domClass.remove(list, "displayNONE"); //那么解除隐藏 evt = setTimeout(function () { //设置定时器延迟(这个是为了防止点击新增的事件会被本次click事件触发) $(document).one("click", function () { //只要点击窗口 domClass.add(list, "displayNONE") //那么就让这个列表因此nag }) }, 50) } else { //如果列表未隐藏(注意,此时有一个定时器的存在) domClass.add(list, "displayNONE"); //那么让列表隐藏 clearTimeout(evt); //并清除定时器,事实上不清除应该也是可以的,只不过domClass会被执行2次(这里和定时器的) } }) } function createExpendList(node, obj) { //创建二级下拉菜单 var list = domConstruct.create("div", { class: "expendlist" }, node); domConstruct.create("span", { class: "expendlistbefore", }, list); obj.forEach(function (item) { var row = domConstruct.create("li", { innerHTML: item.text }, list) var img = domConstruct.create("span", { class: "img", style: "background-image:url(" + item.img + "_unfocus.png)", }, row, "first"); on(row, mouse.enter, function () { domStyle.set(img, "background-image", "url(" + item.img + ".png)"); }) on(row, mouse.leave, function () { domStyle.set(img, "background-image", "url(" + item.img + "_unfocus.png)"); }) }); } }) </script> </head> <body> <div class="parentDiv" id="parentDiv"> <!-- <div class="data"><span class="img" style="background-image:url(test.png)"></span><span class="text">测试3</span><span class="triangle"></span> <div class="list"><span class="before"></span> <div class="row"><span class="img" style="background-image: url("test_unfocus.png");"></span>测试<span class="expendlistTriangle"></span> <div class="expendlist"><span class="expendlistbefore"></span> <li><span class="img" style="background-image: url("test_unfocus.png");"></span>测试</li> <li><span class="img" style="background-image: url("test_unfocus.png");"></span>测试</li> </div> </div> <div class="row"><span class="img" style="background-image: url("test_unfocus.png");"></span>测试 </div> </div> </div>--> </div> </body> </html>
①之前我们有这么一段代码: app.use('/', routes); //假如是根目录,那么交给routes.js来处理; app.use('/users', users); //假如是/users目录,交给users.js来处理 当访问根目录的时候,调用routes;当访问的是users路径是,由users来处理; 然后又知道,当访问其他路径时,会这么处理: app.use(express.static(path.join(__dirname, 'public'))); 即去查看public文件夹下有没有对应的静态页面,如果有,则显示。 那么假如以上都不符合呢?那么会报错。 显然,我们会有这样一种需求,假如当访问/test这样一个路径时,我们需要对她进行一次特殊的处理,例如发送访问时间给用户。 教程上是这么写的: app.use('/test',routes.test); //假如访问的路径是test,那么行为方式是调用routes(即index.js)的test方法 但实际上,最近版本情况下,我们的index.js文件是这样的。 router.get('/', function (req, res, next) { //req是请求,res是回应 res.render('index', {title: 'Express'}); //回应调用render方法 }); 显然,并没有这样一个方法。(教程上老版本的是这样的:index = router.get(略)) 因此我们可以这么做,直接不对app.js做任何操作,但是在index.js添加这样一段代码 router.get('/test', function (req, res, next) { res.send('Hello, your visit time is ' + new Date() + '.'); }) 然后重启路由,我们访问/test路径时,就会显示: Hello, your visit time is Sun Jun 26 2016 15:38:49 GMT+0800 (中国标准时间). 但必须明白这个机制是什么。 ②路由的机制: 错误的认识: 当我们按上面的写法来写的时候,就可能会产生一个错觉: 当访问/test时,由于index.js里面有 router.get('/test', 这样一段代码,无论把这段代码放在哪里,都会交给这段代码来处理访问/test这个路径。例如把这段代码放在users.js里,然后访问http://127.0.0.1/test,也可以获得这样的回应。 当然,事实上并非这样。 正确的认识: 事实上是这么做的,当访问根目录时,即http://127.0.0.1,然后他会去查看routes下(即index.js文件)有有没有这样一段代码: router.get('/' 如果有,则调用其回调函数来进行处理。 如果没有,那么他会返回错误提示。(可能是404页面,也可能是其他错误提示)。 这里的/,指的是app.js中,基础路径: app.use('/', routes); 的根路径。 具体举例的话: 假如我们有一个页面是A页面,我们计划其路径为/base/pageA 【方法一】我们可以这么做: 在app.js中,加入代码: app.use('/', routes); 注意,这里的routes指向的是index.js 然后在index.js里加入代码: router.get('/base/pageA', function (req, res, next) { res.send('Hello, your visit time is ' + new Date() + '.'); }) 我们便可以通过http://127.0.0.1/base/pageA来访问到我们需要访问的网页了 【方法二】 我们也可以这么做: app.js里添加代码 var base = require('./routes/base'); //routes文件夹下的base文件 和 app.use('/base', base); 然后在routes文件夹下新建base.js文件,输入以下代码: var express = require('express'); //调用express模块 var router = express.Router(); //调用模块的Router方法 /* GET home page. */ router.get('/pageA', function (req, res, next) { res.send('Hello, in another page, your visit time is ' + new Date() + '.'); }) module.exports = router; 此时访问网页,将会显示: Hello, in another page, your visit time is Sun Jun 26 201617:08:52 GMT+0800 (中国标准时间). 解释: 我是这么解释的,在app.js里,use函数的第一个参数,其路径表示的是基础路径,而在第二个参数里,js文件的路径是相对路径(相对于这个基础路径的路径)。 假如基础路径是“/app”, 那么相对路径是“/”时,其路径指/app/; 而相对路径是“/test”时,其路径指/app/test; 相对路径是“/index/pageA”时,其路径指/app/index/pageA 注意,根据我测试,似乎不能使用“../test”来访问/test这样的路径 假如路径冲突会如何? 即上面的方法一和方法二同时使用,那么访问http://127.0.0.1/base/pageA的结果是什么? 实践证明,根据app.js来定 由于在app.js里,路由是有先后顺序的,因此哪个在前,就会先交给谁处理。 例如: app.use('/', routes); //假如是根目录,那么交给routes.js来处理; app.use('/base', base); 是index.js相应的代码进行处理; 而 app.use('/base', base); app.use('/', routes); 这样的顺序,是由base.js相应的代码进行处理。 ③路径匹配: 假如用户访问一个这样的路径: /base/myname 其中,myname是用户名,他可能是'abc',也可能是'defg'等(不含引号)。 那么,我们可能就需要获取这个myname。 之前我们遇见的一般是这样的情况: /base?name=abc 我们可以通过url模块的pathname方法来确定其name属性值。 但当前需求显然和之前不同。因此我们使用另外一个方法,在base.js添加以下代码。 router.get('/:username', function (req, res, next) { res.send('username: ' + req.params.username); }) 通过这样的方式,来将myname位置的值,赋值给req的username属性 例如,我们访问:http://127.0.0.1/base/page(注意,避免有直接对该链接进行处理的代码) 会返回我们这样的内容: username: page 根据解释,:username这样的形式,会被自动编译为正则表达式,类似:\/user\/([^\/]+)\/? 这样的形式,而其参数可以在响应函数中通过req.params属性来访问。 另外,路径同样支持javascript的【正则表达式】。因此可以匹配更加复杂的路径,但由于是匿名参数,因此需要通过req.params[0]、req.params[1]来访问了。
①之前有提到,假如有同一路径有两个方法来对其进行处理,那么只有匹配到的第一个方法会被执行,剩余的将略过; 那么有什么办法可以让两个一起执行呢?那就是next 具体而言,如代码: 在index.js中添加代码: router.get('/base/pageA', function (req, res, next) { res.send('index.js.'); next(); }); 在base.js中添加代码 router.get('/pageA', function (req, res) { //res.send('base.js!'); console.log("base.js") }) 注意,之所以注释掉中间那一行,是因为这样写会出错,因此第一个负责返回给用户,第二个负责在控制台显示。 启动app.js,页面显示:index.js. 而控制台显示:base.js ②这种方法的优点: 【1】轻易的实现中间件。(即将A——》B的流程修改为A——》C——》B这样); 【2】提高代码的复用性。例如,我们首先要对一个请求进行检查,如果成立,然后对其进行处理。而这个请求可能是GET,可能是DELETE,也可能是POST或者PUT。 假如每个请求的处理函数,都需要添加一段检查代码,那么显然是不美观的。因此,我们使用all来进行处理,再在all的最后调用next,路由会根据方法的不同,自动跳转到不同的处理函数上。
①REST指Representational State Transfer(表征状态转移),是一种基于HTTP协议的网络应用的接口风格,充分利用HTTP的方法实现统一风格接口的服务。 HTTP协议定义了以下八种标准的方法: 【1】GET 【2】HEAD(请求指定资源的响应头) 【3】POST 【4】PUT 【5】DELETE 【6】TRACE(回显服务器收到的请求,主要用于测试或诊断) 【7】CONNECT(HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器) 【8】OPTIONS(返回服务器支持的HTTP请求方法) 根据REST设计模式,分别是POST增,DELETE删,GET查,PUT改 其中,GET是安全的,即不会对资源产生变动,连续访问多次结果相同; GET、DELETE、PUT是幂等的,即重复多次操作和一次操作,效果是一样的。 而Express支持的HTTP请求的绑定函数如下:
①REST指Representational State Transfer(表征状态转移),是一种基于HTTP协议的网络应用的接口风格,充分利用HTTP的方法实现统一风格接口的服务。 HTTP协议定义了以下八种标准的方法: 【1】GET 【2】HEAD(请求指定资源的响应头) 【3】POST 【4】PUT 【5】DELETE 【6】TRACE(回显服务器收到的请求,主要用于测试或诊断) 【7】CONNECT(HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器) 【8】OPTIONS(返回服务器支持的HTTP请求方法) 根据REST设计模式,分别是POST增,DELETE删,GET查,PUT改 其中,GET是安全的,即不会对资源产生变动,连续访问多次结果相同; GET、DELETE、PUT是幂等的,即重复多次操作和一次操作,效果是一样的。 而Express支持的HTTP请求的绑定函数如下: