0%


title: magiskdate: 2019-12-15 11:10:15

tags: android

magisk

Magisk manager is a magic mask to alter system systemless-ly.

Installing Magisk will give you ROOT, a super powerful Systemless Interface,

Magisk Modules support, and hide from tons of integrity tests like SafetyNet!

What is Magisk?

Magisk is a great open-source root solution for Android devices which is developed by topjohnwu[https://forum.xda-developers.com/member.php?u=4470081], offering a “Systemless Interface”, allowing easy modification of your favourite device with peace of mind. Magisk is a module based application, so its configuration can be as individual as the member applying them. Many modules exist for Magisk, providing all sorts of modifications, such as cloaking your device from SafetyNet.

Highlights

Open Source:

100% open source and easy to build on all Google supported platforms.

Magic Mount:

Modify system (vendor) without tampering the partitions.

MagiskSU:

Unleash your device’s root access.

MagiskHide:

Hide Magisk from detections: Google SafetyNet, enterprise / bank system integrity checks, game tamper checks.

Resetprop:

Modify or delete any system properties, including read-only props.

Magisk Modules:

Install Magisk Modules from the community driven Magisk Module Repo.Many custom modules for Magisk,like Fonts,Audio Mod,DNS.

What is Magisk Manager?

Magisk Manager is the application same as SuperSU which is pre-installed on your device after you have rooted your Android device. If you want to use the magisk manager you can unroot your device and then root it with the help flashing Magisk-v20.zip or else you can install the magisk manager application and do the system less rooting on your device.

There are many questions raised, and people has a query that is How to root with magisk? The rooting procedure is simple, and you need a custom recovery installed on your device. If you have the custom recovery, then you can root your Android device by flashing the Magisk-v20.zip file.

Magisk and superSu

If you root your device with the help of magisk, then the magisk won’t do any changes in your system files. Magisk will modify the boot.img to magisk.img. Magisk won’t add any files in your system files.

When SuperSU roots your device, it will do the changes in the System files and also adds some files in the system partition. If you install SuperSU, you won’t be getting any OTA updates or any financial application won’t work on your device.

guide

Before Installing Magisk,you must Unlocking Bootloader.

Download Magisk Zip and Magisk Manager APK

You can usually download Magisk from the original development thread at XDA. But it only provides Magisk download link for the latest version. However, sometimes, the latest version isn’t the best version for you. For instance, a newer version of Magisk may not be able to hide from an app like Google Pay which is actually known to run into problems despite using MagiskHide.

Using an older version of Magisk that you know works for you might be a temporary solution until it’s fixed. In that case, though, you’re out of luck. But not anymore. You can download all Magisk versions from below. The list below is comprehensive and contains all Magisk versions since it was released publicly.

Below, you will find oldest to the latest version of Magisk zip for Android Nougat, Oreo, and Pie and download them directly from the Magisk Github repository. Just click the direct Magisk download links below. Moreover, we’ve also listed the compatible version of Magisk Manager and Magisk Uninstaller for each Magisk release. You can use Magisk Uninstaller zip file to remove Magisk from your Android device.

How to install Magisk?

If you don’t have a Android device with a TWRP recovery,you can refer to Plan A.But I suggest you install it,it’s very userful.

If you have a Android device with a TWRP recovery,you can refer to Plan B.

If you are using a Huawei device running EMUI 8 and higher, please check its section.

If you are using a Samsung device that is launched with Android 9.0 (new devices in 2019), please check its section.

Plan A

  1. Download Magisk Manager on your Android device.
  2. Install the application, and you might get an unknown source warning. You need to turn on Unknown Sources to turn it on, click on “Settings”.
  3. Now, try to repeat the installation process again. Try installing the application and open it.
  4. If you have already installed the Chainfire SuperSU on your device, then you need to grant the root permission.
  5. Now, click on the Install button to install the magisk on your device.
  6. If you will click on Install button you will see that it will ask you to select method.
  7. Just click on Direct Install if you want to directly install the file on your phone without using custom recovery on your phone, or you can click on Download Zip File Only.
  8. Now, if the download is the done. Magisk Manager will automatically install the file on your phone.

Plan B

  1. Download the Magisk zip file.
  2. Place the zip file in your internal storage. Make sure that you remember the proper location of the zip file.
  3. Reboot your phone into recovery ensure that you have a custom recovery such as TWRP is installed on your phone.
  4. Now, click on the Install button in the TWRP recovery.
  5. Navigate the Magisk-v20.zip on your internal storage or SD card.
  6. Now, install the zip file on your device and wait till it is getting installed on your device.
  7. You have successfully flashed the Magisk-v20.zip on your device.
  8. Reboot your phone and see if it works on your device.
  9. Download the magisk manager application from the link given above.
  10. Install the application by following the above procedure.
  11. Open it and then you will see magisk is installed on your Android device.

How to use Magisk Hide?

Magisk Hide is the feature which is available in the Magisk Manager application and if you want to hide the root permission for the particular banking application.

You can turn the Magisk hide option from the settings of the magisk application then you can easily enjoy your favorite app without any issues.

If you want to activate the Magisk Hide option, then you can follow the below tutorial, and you can hide the root permission on your device.

  1. First, check the application which is not working on your rooted Android device.
  2. Open Magisk on your device and click on the Menu button.
  3. Now, Click on the Settings to turn on the magisk hide option.
  4. Now, scroll down and enable the Magisk Hide option.
  5. Press the Menu key again and click on the “Magisk Hide” option.
  6. Now, you need to select the application from which you need to hide the root.
  7. Open your banking application and then that’s it. You can run the banking application on your device now.

Huawei

Huawei devices using Kirin processors have a different partitioning method from most common devices. Magisk is usually installed to the boot partition of the device, however Huawei devices do not have this partition. Depending on what EMUI version your device is running, the instructions will be slightly different.

Obtain Stock Images

Huawei does not release official factory images, however most firmware zips can be downloaded from the Huawei Firmware Database. To extract images from UPDATE.APP in the zip, you have to use Huawei Update Extractor (Windows only!)

EMUI 8

For EMUI 8 devices, your device has a partition named ramdisk, which is where Magisk is going to be installed.

If you plan to use custom recoveries, simply follow the instructions for custom recovery and you’re all set.

If you plan not to use custom recoveries, you will have to extract RAMDISK.img from your firmware. Follow the instructions for boot image patching above, but use the RAMDISK.img file instead of a boot image.

To flash the patched image to your device, here is the fastboot command:

fastboot flash ramdisk /path/to/magisk_patched.img

Be aware you are flashing to ramdisk, not boot!

EMUI 9 or Higher

For EMUI 9+ devices, the ramdisk partition no longer exists. As a workaround, Magisk will be installed to the recovery_ramdisk partition. Please read the Magisk in Recovery section before following the instructions below!

Note: As I tested on my Honor View 10, Huawei’s kernel does not seem to be able to capture key button press events in early boot, so long pressing Volume Up does NOT boot to recovery on my device. Your experience may vary.

If you plan to use custom recoveries, simply follow the instructions for custom recovery and you’re all set.

Warning: Magisk will overwrite the custom recovery.

If you plan not to use custom recoveries, you will have to extract RECOVERY_RAMDIS.img from your firmware. Follow the instructions for boot image patching above, but use the RECOVERY_RAMDIS.img file instead of a boot image.

To flash the patched image to your device, here is the fastboot command:

fastboot flash recovery_ramdisk /path/to/magisk_patched.img

Be aware you are flashing to recovery_ramdisk, not boot!

Samsung (System-as-root)

If your device is NOT launched with Android 9.0 or higher (released after 2019), you are reading the wrong section.

Before Installing Magisk

Your device is non-A/B and uses system-as-root, so Magisk will be installed to the recovery partition of your device. Please read the Magisk in Recovery section!

Installing Magisk WILL trip KNOX

Installing Magisk for the first time REQUIRES a full data wipe, backup before continue

You have to have your bootloader unlocked before following the instructions

Unlocking Bootloader

Normally I wouldn’t provide instructions for this, but since things had changed drastically from previous Samsung devices, and there are some caveats, I figure this would be helpful.

Allow bootloader unlocking in Developer options → OEM unlocking

Reboot your device to download mode. Either use adb reboot download, or use the key combo for your device.

Long press volume up to unlock the bootloader. This will wipe your data and automatically reboot.

Just when you think the bootloader is unlocked, it is actually not! Samsung introduced VaultKeeper, meaning the bootloader will reject any unofficial partitions before VaultKeeper explicitly allows it.

Go through the initial setup. Skip through all the steps since data will be wiped again later when we are installing Magisk. Connect the device to internet in the setup!

Enable developer options, and confirm that the OEM unlocking option exists and grayed out! The VaultKeeper service will unleash the bootloader after it confirms that the user has the OEM unlocking option enabled.

Your bootloader now accepts unofficial images in download mode.

Instructions

Download the firmware for your device.

Unzip the firmware and copy the AP tar file to your device. It is normally named as AP_[device_model_sw_ver].tar.md5

Install the latest Magisk Manager

In Magisk Manager: Install → Install → Select and Patch a File and select the AP tar file.

Magisk Manager will patch the whole firmware file and store the output to [Internal Storage]/Download/magisk_patched.tar

Copy the patched file to your PC with adb pull /sdcard/Download/magisk_patched.tar. Do not use MTP as it is reported to corrupt files.

Reboot to download mode, and flash magisk_patched.tar as AP in Odin, together with the BL, CP and HOME_CSC files. Never flash only an AP file, as Odin can shrink your /data file-system if you do.

Important: Uncheck “Auto Reboot” in Options!

Magisk is now successfully flashed to your device! But there are still several steps before you can properly use the device.

We now want to boot into the stock recovery to factory reset our device.

Full data wipe is mandatory! Do not skip this step.

Press Power + Volume Down to exit download mode. As soon as the screen turns off, immediately press the combo key to boot to recovery (e.g. on the S10 it is Power + Bixby + Volume Up). Since we want to boot into stock recovery, continue pressing the volume up button until you see the stock recovery screen.

Use volume buttons to navigate through the stock recovery menu, and the power button to select an option. Choose Wipe data/factory reset to wipe the data of the device.

This time, we can finally boot to the system with Magisk. Select Reboot system now, and immediately press the combo key to recovery. After seeing the bootloader warning screen, release all buttons so it can boot to the system.

The device will automatically reboot for the first time it boots. This is completely normal and done by design.

After the device is booted up, do the usual initial setup. The following steps will need an internet connection.

You shall see Magisk Manager in your app drawer; if not, manually install the APK you downloaded in step 3 and continue to the next step. The app would be a stub and it shall automatically upgrade to the full Magisk Manager when you open it.

Magisk Manager will ask to do additional setups. Let it do its job and the app will automatically reboot your device.

Voila! Enjoy Magisk :)

Additional Info

Magisk actually patches 3 partitions on your device:

vbmeta: replace with empty vbmeta image to disable partition verification

boot: remove the signature of the image to prevent soft bricks

recovery: this is where Magisk is actually installed

Never, ever try to restore either of the 3 images mentioned back to stock! You can easily brick your device by doing so, and the only way out is to do full Odin restore following with factory reset. Just don’t do it.

If you want to upgrade your device, never flash the stock AP tar file with the reasons mentioned above. Always pre-patch the firmware before flashing in Odin.

If you don’t need to patch the full firmware, you can manually create a tar file with at least vbmeta.img, boot.img, and recovery.img to let Magisk Manager patch your images in the proper way.

Magisk Modules


title: 前端工程化

date: 2019-09-01 14:26:50

tags: javascript

categories:  # 这里写的分类会自动汇集到 categories 页面上,分类可以多级

  • JS # 一级分类
  • 前端工程化 # 二级分类

模块化初探

模块化系统的风格

模块化的价值

最主要的目的:

  • 解决命名冲突
  • 依赖管理

其他价值:

  • 提高代码可读性
  • 代码解耦,提高复用性

<script>标签风格

1
2
3
<script src="module1.js"></script>
<script src="module2.js"></script>
<script src="module3.js"></script>

各个模块把接口暴露给全局对象,比如 window.各个模块通过全局对象进行相互访问

出现的问题:

  1. 全局对象的冲突
  2. 加载的顺序很重要
  3. 开发者需要解决模块的依赖问题
  4. 在大项目中模块非常多难以维护

CommonJs: 同步的 require

CommonJs 是 Node 独有的规范,浏览器中使用就需要用到 Browserify 解析了。

使用同步的require方法来加载依赖和返回暴露的接口.一个模块可以通过exports对象添加属性,或者设置module.exports的值来描述暴露对象.

1
2
3
4
require("moudle");
require("../file.js");
exports.doStuff = function () {};
moudle.exports = someValue;

优点:

  1. 服务端代码可以被复用.
  2. npm 有大量的代码
  3. 使用方便

缺点:

  1. 阻塞调用无法在网络环境应用,网络请求是异步的
  2. 不能并行require多个模块

CommonJS 规范

  1. 在一个模块中,存在一个自由的变量”require”,它是一个函数。

这个”require”函数接收一个模块标识符。

“require”返回外部模块所输出的 API。 2. 在一个模块中,会存在一个名为”exports”的自由变量,它是一个对象,模块可以在执行的时候把自身的 API 加入到其中。 3. 模块必须使用”exports”对象来做为输出的唯一表示。

AMD 规范: 异步的 require

AMD 主要解决两个问题:

  1. 多个 js 文件可能有依赖关系,被依赖的文件需要早于依赖它的文件加载到浏览器
  2. js 加载的时候浏览器会停止页面渲染,加载文件越多,页面失去响应时间越长

requireJS 语法

RequireJS 遵循 AMD 规范,用于解决命名冲突和文件依赖的问题.

requireJS 定义了一个函数 define,它是全局变量,用来定义模块

define(id?, dependencies?, factory);

  1. id:可选参数,用来定义模块的标识,如果没有提供该参数,脚本文件名(去掉拓展名)
  2. dependencies:是一个当前模块依赖的模块名称数组
  3. factory:工厂方法,模块初始化要执行的函数或对象。如果为函数,它应该只被执行一次。如果是对象,此对象应该为模块的输出值

在页面上使用 require 函数加载模块

require([dependencies], function(){});

require()函数接受两个参数:

  1. 第一个参数是一个数组,表示所依赖的模块
  2. 第二个参数是一个回调函数,当前面指定的模块都加载成功后,它将被调用。加载的模块会以参数形式传入该函数,从而在回调函数内部就可以使用这些模块.

require()函数在加载依赖的函数的时候是异步加载的,这样浏览器不会失去响应,它指定的回调函数,只有前面的模块都加载成功后,才会运行,解决了依赖性的问题。

1
2
3
4
5
6
require(["moudle", "../file"], function (moudle, file) {
/* ... */
});
define("moudle", ["dep1", "dep2"], function (d1, d2) {
return someExportedValue;
});

importexport的区别

以前:

CommonJS(服务器) 和 AMD(浏览器)规范各自实现了运行时加载模块的方法(没办法在编译时做“静态优化”)。

1
2
3
4
5
6
7
// CommonJS模块
let { stat, exists, readFile } = require("fs");
// 等同于
let _fs = require("fs");
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;

现在:

ES6:一个模块就是一个文件,export/import命令可以出现在模块的任何位置,只要处于模块顶层就可以。

通过importexport实现静态加载(编译时加载),服务端和浏览器通用。

1
2
// ES6模块
import { stat, exists, readFile } from "fs";

export:如果你希望外部能够读取模块内部的某个变量,就必须使用 export 关键字输出该变量

1
2
3
4
5
// profile.js
var firstName = "Michael";
var lastName = "Jackson";
var year = 1958;
export { firstName, lastName, year };

还可以输出函数和 class 类:

1
2
3
export function multiply(x, y) {
return x * y;
}

还可以改个名字输出:

1
2
3
4
5
6
7
function v1() { ... }
function v2() { ... }
export {
v1 as streamV1,
v2 as streamV2,
v2 as streamLatestVersion
};

需要特别注意的是,export 命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。

export 语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。

CommonJS 模块输出的是值的缓存,不存在动态更新。

import:加载模块

1
2
3
4
5
// main.js
import { firstName, lastName, year } from "./profile";
function setName(element) {
element.textContent = firstName + " " + lastName;
}

可以改名字:

1
import { lastName as surname } from "./profile";

注意,import 命令具有提升效果,会提升到整个模块的头部,首先执行。import 命令是编译阶段执行的,在代码运行之前。

由于 import 是静态执行,所以不能使用表达式和变量。

会执行加载的模块:

1
import "lodash"; //仅仅执行lodash模块,但是不输入任何值。多次引入同一个模块仅执行一次。

整体加载所有的模块:

1
import * as circle from "./circle";

export default:为模块指定默认输出,引用的时候不用知道输出的到底是什么,可以指定任意名字。

需要注意的是,这时 import 命令后面,不使用大括号。一个模块只能有一个默认输出。

1
2
3
4
5
6
7
// export-default.js
export default function () {
console.log("foo");
}
// import-default.js
import customName from "./export-default";
customName(); // 'foo'

同时引用默认模块和其他模块:

1
2
3
4
5
6
7
8
9
10
//输出
export default function (obj) {
// ···
}
export function each(obj, iterator, context) {
// ···
}
export { each as forEach };
//对应的加载
import _, { each, each as forEach } from "lodash";

ES6 模块加载 CommonJS 模块:

1
2
3
4
5
6
7
8
9
10
11
//commonjs模块:
// a.js
module.exports = {
foo: "hello",
bar: "world",
};
// 等同于
export default {
foo: "hello",
bar: "world",
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//es6加载上面的模块:
// 写法一
import baz from "./a";
// baz = {foo: 'hello', bar: 'world'};
// 写法二
import { default as baz } from "./a";
// baz = {foo: 'hello', bar: 'world'};
// 写法三
import * as baz from "./a";
// baz = {
// get default() {return module.exports;},
// get foo() {return this.default.foo}.bind(baz),
// get bar() {return this.default.bar}.bind(baz)
// }

CommonJS 加载 ES6 模块:通过import()函数

1
2
3
4
// es6.mjs
let foo = { bar: "my-default" };
export default foo;
foo = null;
1
2
3
4
5
6
7
8
9
// commonjs.js
const es_namespace = await import("./es");
// es_namespace = {
// get default() {
// ...
// }
// }
console.log(es_namespace.default);
// { bar:'my-default' }

import 和 require 的区别

#module.exports 和 export default 的区别

webpack

webpack 是收把项目当作一个整体,通过一个给定的的主文件,webpack 将从这个文件开始找到你的项目的所有依赖文件,使用 loaders 处理它们,最后打包成一个或多个浏览器可识别的 js 文件.

初始化

  1. 新建一个文件夹,cd 到它的目录下.执行npm init -y命令
  2. 执行npm install --save--dev webpack命令安装(也可以全局安装)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/配置
touch webpack.config.js
vi webpack.config.js
//在里面写以下内容
/*
const path = require('path');
module.exports = {
entry: './src/index.js', //入口
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
}
};*/

//创建文件
touch src/index.js

//运行webpack
npx webpack //这时会多出dist目录,里面有bundle.js文件

2.使用

1.在 index.js 里写

1
2
3
4
5
6
console.log(1)

//再运行webpack:
npx webpack

//再看bundle.js,这时会多出来一行console.log(1)

2.安装 babel-loader 自动转换 es6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//安装v6,命令行
npm install babel-loader babel-core babel-preset-env webpack

//将这个复制到webpack的配置文件webpack.config.js里,加在output的下面
module: {
rules: [
{
test: /\.js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: ['env']
}
}
}
]
}

//加在output的下面,复制完后成这样
const path = require('path');

module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: ['env']
}
}
}
]
}
};

运行 npx webpack

若出现 can’t find ‘…’或 can’t resolve ‘…’的报错,则安装省略号里的东西 npm i ‘省略号’

注意:若是 Couldn’t find preset “env”,不要安装 env,而是 npm i babel-preset-env

3.使用 babel

1
2
3
//当你在写index.js里写
let a = 1;
//它就会帮你自动转换成es5了

NPM 脚本(NPM Scripts)

在 package.json 文件中写入 npm 脚本,就可以使用npm run build替代npx命令.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  {
"name": "webpack-demo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
+ "build": "webpack"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"webpack": "^4.0.1",
"webpack-cli": "^2.0.9",
"lodash": "^4.17.5"
}
}

管理资源(css,图片之类)

加载 CSS

  1. 需要先安装 style-laoder 和 css-loader

npm install --save-dev style-loader css-loader

  1. 然后在webpack.config.js文件里添加规则
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  const path = require('path');

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

webpack 根据正则表达式,来确定应该查找哪些文件,并将其提供给指定的 loader。在这种情况下,以 .css 结尾的全部文件,都将被提供给 style-loader 和 css-loader。

这使你可以在依赖于此样式的文件中import './style.css'。现在,当该模块运行时,含有 CSS 字符串的<style>标签,将被插入到 html 文件的 <head>中。

加载图片

  1. 安装npm install --save-dev file-loader插件
  2. 配置webpack.config.js文件

现在,当你 import MyImage from './my-image.png',该图像将被处理并添加到 output 目录,

并且 MyImage 变量将包含该图像在处理后的最终 url。当使用 css-loader 时,如上所示,

你的 CSS 中的url('./my-image.png') 会使用类似的过程去处理。loader 会识别这是一个本地文件,

并将 './my-image.png'路径,替换为输出目录中图像的最终路径。

html-loader 以相同的方式处理 <img src="./my-image.png" />

  1. 在 src 文件夹下创建 icon.png.

  2. 修改src/index.js文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  import _ from 'lodash';
import './style.css';
+ import Icon from './icon.png';

function component() {
var element = document.createElement('div');

// Lodash,现在由此脚本导入
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
element.classList.add('hello');

+ // 将图像添加到我们现有的 div。
+ var myIcon = new Image();
+ myIcon.src = Icon;
+
+ element.appendChild(myIcon);

return element;
}

document.body.appendChild(component());
  1. src/style.css中引入图片
1
2
3
4
  .hello {
color: red;
+ background: url('./icon.png');
}
  1. 重新构建npm run build

Glup

简介

它是一款 nodejs 应用。

它是打造前端工作流的利器,打包、压缩、合并、git、远程操作…,

简单易用

无快不破

高质量的插件

安装

安装 gulp

npm install -g gulp

如果报Error: EACCES, open '/Users/xxx/xxx.lock错误。

先执行:sudo chown -R $(whoami) $HOME/.npm

如果使用 npm 安装插件太慢(被墙),

可执行 npm install -g cnpm --registry=https://registry.npm.taobao.org

先安装 cnpm, 之后再安装插件时用 cnpm 安装cnpm install gulp

  1. 安装各种插件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
npm install --save gulp            //本地使用gulp
npm install --save gulp-imagemin //压缩图片
npm install --save gulp-minify-css //压缩css
npm install --save gulp-ruby-sass //sass
npm install --save gulp-jshint //js代码检测
npm install --save gulp-uglify //js压缩
npm install --save gulp-concat //文件合并
npm install --save gulp-rename //文件重命名
npm install --save png-sprite //png合并
npm install --save gulp-htmlmin //压缩html
npm install --save gulp-clean //清空文件夹
npm install --save browser-sync //文件修改浏览器自动刷新
npm install --save gulp-shell //执行shell命令
npm install --save gulp-ssh //操作远程机器
npm install --save run-sequence //task顺序执行

或者根据 package.json 自动安装。把 package.json 拷贝到自己的工程目录下,进入目录,执行:npm install

语法 gulp API

gulp.src(globs[,options])

1
2
3
4
5
gulp
.src("client/templates/*.jade")
.pipe(jade())
.pipe(minify()) //压缩文件
.pipe(gulp.dest("build/minified_templates"));

作用: 输出符合所提供的匹配模式或数组的文件.

globs 类型: string 或 Array

所要读取的 glob 或者包含 globs 的数组.可以是地址

options 类型:object

额外的选项参数

gulp.dest(path[,options])

1
2
3
4
5
6
gulp
.src("./client/templates/*.jade")
.pipe(jade())
.pipe(gulp.dest("./build/templates"))
.pipe(minify())
.pipe(gulp.dest("./build/minified_templates"));

文件被 pipe 进来,dest 生成或者写入一个文件.

path

文件将被写入的路径(输出目录)。

gulp.task(name[,deps],fn)

定义一个实现任务

1
2
3
gulp.task('somename', function(){
//任务
})

name

任务名字

deps 类型: array

一个包含任务列表的数组,这些任务会在你当前运行任务之前完成.

1
2
3
gulp.task('mytask', ['array', 'of', 'task', 'names'], function(){
//任务
})

fn

该函数定义任务所要执行的一些操作。通常来说,它会是这种形式:gulp.src().pipe(someplugin())。

gulp.watch(glob[, opts], tasks)

监视文件,并且可以在文件发生改动时候做一些事情。它总会返回一个 EventEmitter 来发射(emit) change 事件。

1
2
3
4
5
6
var watcher = gulp.watch("js/**/*.js", ["uglify", "reload"]);
watcher.on("change", function (event) {
console.log(
"File " + event.path + " was " + event.type + ", running tasks..."
);
});

gulp 使用实例

范例 1. 压缩合并

demo1 目录结构如下。把 demo1 中的 index.html 压缩,把 src 里面的 less 编译、合并、压缩、重命名、存储到 dist。src 里面的图片压缩、合并存储到 dist。src 里面的 js 做代码检查,压缩,合并,存储到 dist。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
+ demo1
+ dist
+ css
- merge.min.css
+ js
- merge.min.js
+ imgs
- 1.png
- 2.png
- index.html
+ src
+ css
- a.css
- b.css
+ js
- a.js
- b.js
+ imgs
- 1.png
- 2.png
- index.html

创建 gulpfile.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
var gulp = require("gulp");

// 引入组件
var minifycss = require("gulp-minify-css"), // CSS压缩
uglify = require("gulp-uglify"), // js压缩
concat = require("gulp-concat"), // 合并文件
rename = require("gulp-rename"), // 重命名
clean = require("gulp-clean"), //清空文件夹
minhtml = require("gulp-htmlmin"), //html压缩
jshint = require("gulp-jshint"), //js代码规范性检查
imagemin = require("gulp-imagemin"); //图片压缩

gulp.task("html", function () {
return gulp
.src("src/*.html")
.pipe(minhtml({ collapseWhitespace: true })) //html压缩
.pipe(gulp.dest("dist"));
});

gulp.task("css", function (argument) {
gulp
.src("src/css/*.css")
.pipe(concat("merge.css")) // CSS压缩
.pipe(
rename({
suffix: ".min", // 重命名
})
)
.pipe(minifycss()) // CSS压缩
.pipe(gulp.dest("dist/css/"));
});
gulp.task("js", function (argument) {
gulp
.src("src/js/*.js")
.pipe(jshint()) //js代码规范性检查
.pipe(jshint.reporter("default"))
.pipe(concat("merge.js")) // 合并文件
.pipe(
rename({
suffix: ".min", // 重命名
})
)
.pipe(uglify()) // js压缩
.pipe(gulp.dest("dist/js/"));
});

gulp.task("img", function (argument) {
gulp
.src("src/imgs/*")
.pipe(imagemin()) //图片压缩
.pipe(gulp.dest("dist/imgs"));
});

gulp.task("clear", function () {
gulp.src("dist/*", { read: false }).pipe(clean()); //清空文件夹
});

gulp.task("build", ["html", "css", "js", "img"]);

最后命令行gulp build;

可实现 src 目录下的 html 压缩,css、js 合并压缩,图片压缩,最后放入 dist 目录下

范例 2. 监控变动自动同步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
var gulp = require("gulp");

// 引入组件
var browserSync = require("browser-sync").create(); //用于浏览器自动刷新
var scp = require("gulp-scp2"); //用于scp到远程机器
var fs = require("fs");

gulp.task("reload", function () {
browserSync.reload();
});

gulp.task("server", function () {
browserSync.init({
server: {
baseDir: "./src",
},
});

gulp.watch(["**/*.css", "**/*.js", "**/*.html"], ["reload", "scp"]);
});

gulp.task("scp", function () {
return gulp
.src("src/**/*")
.pipe(
scp({
host: "121.40.201.213",
username: "root",
privateKey: fs.readFileSync("/Users/wingo/.ssh/id_rsa"),
dest: "/var/www/fe.jirengu.com",
watch: function (client) {
client.on("write", function (o) {
console.log("write %s", o.destination);
});
},
})
)
.on("error", function (err) {
console.log(err);
});
});

命令行执行:

1
2
gulp scp;  // 可把本地开发环境代码拷贝到服务器
gulp server; //可在本地创建服务器,本地开发浏览器立刻刷新

范例 3.   监控项目文件变动,自动压缩、合并、打包、添加版本号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
html

</html>
<head>

<!-- build:css css/merge.css -->
<link href="css/a.css" rel="stylesheet">
<link href="css/b.css" rel="stylesheet">
<!-- endbuild -->

</head>
<body>

<p>demo1-工程化手动版</p>

<!-- build:js js/merge.js -->
<script type="text/javascript" src="js/a.js"></script>
<script type="text/javascript" src="js/b.js"></script>
<!-- endbuild -->

</body>
</html>

设置 gulpfile.js 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
var gulp = require("gulp");
var rev = require("gulp-rev"); //添加版本号
var revReplace = require("gulp-rev-replace"); //版本号替换
var useref = require("gulp-useref"); //解析html资源定位
var filter = require("gulp-filter"); //过滤数据
var uglify = require("gulp-uglify");
var csso = require("gulp-csso"); //css优化压缩
var clean = require("gulp-clean");

gulp.task("index", ["clear"], function () {
var jsFilter = filter("**/*.js", { restore: true });
var cssFilter = filter("**/*.css", { restore: true });

var userefAssets = useref.assets();

return gulp
.src("src/index.html")
.pipe(userefAssets) // Concatenate with gulp-useref
.pipe(jsFilter)
.pipe(uglify()) // Minify any javascript sources
.pipe(jsFilter.restore)
.pipe(cssFilter)
.pipe(csso()) // Minify any CSS sources
.pipe(cssFilter.restore)
.pipe(rev()) // Rename the concatenated files
.pipe(userefAssets.restore())
.pipe(useref())
.pipe(revReplace()) // Substitute in new filenames
.pipe(gulp.dest("dist"));
});

gulp.task("clear", function () {
gulp.src("dist/*", { read: false }).pipe(clean());
});

范例 4. 本地 shell 命令, 远程 shell, 任务顺序执行…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
var gulp = require("gulp");

var shell = require("gulp-shell");
var runSequence = require("run-sequence");
var fs = require("fs");
var GulpSSH = require("gulp-ssh");

//shell操作,
gulp.task(
"git",
shell.task(["git add .", 'git commit -am "dd"', "git push -u origin dev"])
);
gulp.task(
"clear",
shell.task(['find . -name ".DS_Store" -depth -exec rm {} \\;'])
);

//操作远程主机
var gulpSSH = new GulpSSH({
ignoreErrors: false,
sshConfig: {
host: "121.40.201.213",
port: 22,
username: "root",
privateKey: fs.readFileSync("/Users/wingo/.ssh/id_rsa"),
},
});

gulp.task("remote", function () {
return gulpSSH.shell([
"cd /var/www/fe.jirengu.com",
"git pull origin dev",
"rm -rf _runtime",
]);
});

gulp.task("build", function (callback) {
runSequence("git", "clear", "remote", callback);
});

gulp.task("watch", function () {
gulp.watch(["**/*.css", "**/*.js", "**/*.html", "**/*.php"], ["build"]);
});

webpack 相关问题

  1. webpack 与 grunt、gulp 的不同

三者都是前端构建工具,grunt 和 gulp 在早期比较流行,现在 webpack 相对来说比较主流,不过一些轻量化的任务还是会用 gulp 来处理,比如单独打包 CSS 文件等。

grunt 和 gulp 是基于任务和流(Task、Stream)的。类似 jQuery,找到一个(或一类)文件,对其做一系列链式操作,更新流上的数据, 整条链式操作构成了一个任务,多个任务就构成了整个 web 的构建流程。

webpack 是基于入口的。webpack 会自动地递归解析入口所需要加载的所有资源文件,然后用不同的 Loader 来处理不同的文件,用 Plugin 来扩展 webpack 功能。

所以总结一下:

从构建思路来说

gulp 和 grunt 需要开发者将整个前端构建过程拆分成多个Task,并合理控制所有Task的调用关系

webpack 需要开发者找到入口,并需要清楚对于不同的资源应该使用什么 Loader 做何种解析和加工

对于知识背景来说

gulp 更像后端开发者的思路,需要对于整个流程了如指掌 webpack 更倾向于前端开发者的思路

  1. 与 webpack 类似的工具还有哪些?谈谈你为什么最终选择(或放弃)使用 webpack?

同样是基于入口的打包工具还有以下几个主流的:

webpack

rollup

parcel

从应用场景上来看:

webpack 适用于大型复杂的前端站点构建

rollup 适用于基础库的打包,如 vue、react

parcel 适用于简单的实验性项目,他可以满足低门槛的快速看到效果

由于 parcel 在打包过程中给出的调试信息十分有限,所以一旦打包出错难以调试,所以不建议复杂的项目使用 parcel

  1. 有哪些常见的 Loader?他们是解决什么问题的?

file-loader:把文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件

url-loader:和 file-loader 类似,但是能在文件很小的情况下以 base64 的方式把文件内容注入到代码中去

source-map-loader:加载额外的 Source Map 文件,以方便断点调试

image-loader:加载并且压缩图片文件

babel-loader:把 ES6 转换成 ES5

css-loader:加载 CSS,支持模块化、压缩、文件导入等特性

style-loader:把 CSS 代码注入到 JavaScript 中,通过 DOM 操作去加载 CSS。

eslint-loader:通过 ESLint 检查 JavaScript 代码

  1. 有哪些常见的 Plugin?他们是解决什么问题的?

define-plugin:定义环境变量

commons-chunk-plugin:提取公共代码

uglifyjs-webpack-plugin:通过 UglifyES 压缩 ES6 代码

  1. Loader 和 Plugin 的不同?

不同的作用

Loader 直译为”加载器”。Webpack 将一切文件视为模块,但是 webpack 原生是只能解析 js 文件,如果想将其他文件也打包的话,就会用到 loader。 所以 Loader 的作用是让 webpack 拥有了加载和解析非 JavaScript 文件的能力

Plugin 直译为”插件”。Plugin 可以扩展 webpack 的功能,让 webpack 具有更多的灵活性。 在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。

不同的用法

Loader 在 module.rules 中配置,也就是它作为模块的解析规则而存在。 类型为数组,每一项都是一个 Object,里面描述了对于什么类型的文件(test),使用什么加载(loader)和使用的参数(options)

Plugin 在 plugins 中单独配置。 类型为数组,每一项是一个 plugin 的实例,参数都通过构造函数传入。

  1. webpack 的构建流程是什么?从读取配置到输出文件这个过程尽量说全

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

初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数;

开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译;

确定入口:根据配置中的 entry 找出所有的入口文件;

编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;

完成模块编译:在经过第 4 步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系;

输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;

输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。

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

  1. 是否写过 Loader 和 Plugin?描述一下编写 loader 或 plugin 的思路?

Loader 像一个”翻译官”把读到的源文件内容转义成新的文件内容,并且每个 Loader 通过链式操作,将源文件一步步翻译成想要的样子。

编写 Loader 时要遵循单一原则,每个 Loader 只做一种”转义”工作。 每个 Loader 的拿到的是源文件内容(source),可以通过返回值的方式将处理后的内容输出,也可以调用 this.callback()方法,将内容返回给 webpack。 还可以通过 this.async()生成一个 callback 函数,再用这个 callback 将处理后的内容输出出去。 此外 webpack 还为开发者准备了开发 loader 的工具函数集——loader-utils。

相对于 Loader 而言,Plugin 的编写就灵活了许多。 webpack 在运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。

  1. webpack 的热更新是如何做到的?说明其原理?

webpack 的热更新又称热替换(Hot Module Replacement),缩写为 HMR。 这个机制可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块。

原理:

首先要知道 server 端和 client 端都做了处理工作

第一步,在 webpack 的 watch 模式下,文件系统中某一个文件发生修改,webpack 监听到文件变化,根据配置文件对模块重新编译打包,并将打包后的代码通过简单的 JavaScript 对象保存在内存中。

第二步是 webpack-dev-server 和 webpack 之间的接口交互,而在这一步,主要是 dev-server 的中间件 webpack-dev-middleware 和 webpack 之间的交互,webpack-dev-middleware 调用 webpack 暴露的 API 对代码变化进行监控,并且告诉 webpack,将代码打包到内存中。

第三步是 webpack-dev-server 对文件变化的一个监控,这一步不同于第一步,并不是监控代码变化重新打包。当我们在配置文件中配置了 devServer.watchContentBase 为 true 的时候,Server 会监听这些配置文件夹中静态文件的变化,变化后会通知浏览器端对应用进行 live reload。注意,这儿是浏览器刷新,和 HMR 是两个概念。

第四步也是 webpack-dev-server 代码的工作,该步骤主要是通过 sockjs(webpack-dev-server 的依赖)在浏览器端和服务端之间建立一个 websocket 长连接,将 webpack 编译打包的各个阶段的状态信息告知浏览器端,同时也包括第三步中 Server 监听静态文件变化的信息。浏览器端根据这些 socket 消息进行不同的操作。当然服务端传递的最主要信息还是新模块的 hash 值,后面的步骤根据这一 hash 值来进行模块热替换。

webpack-dev-server/client 端并不能够请求更新的代码,也不会执行热更模块操作,而把这些工作又交回给了 webpack,webpack/hot/dev-server 的工作就是根据 webpack-dev-server/client 传给它的信息以及 dev-server 的配置决定是刷新浏览器呢还是进行模块热更新。当然如果仅仅是刷新浏览器,也就没有后面那些步骤了。

HotModuleReplacement.runtime 是客户端 HMR 的中枢,它接收到上一步传递给他的新模块的 hash 值,它通过 JsonpMainTemplate.runtime 向 server 端发送 Ajax 请求,服务端返回一个 json,该 json 包含了所有要更新的模块的 hash 值,获取到更新列表后,该模块再次通过 jsonp 请求,获取到最新的模块代码。这就是上图中 7、8、9 步骤。

而第 10 步是决定 HMR 成功与否的关键步骤,在该步骤中,HotModulePlugin 将会对新旧模块进行对比,决定是否更新模块,在决定更新模块后,检查模块之间的依赖关系,更新模块的同时更新模块间的依赖引用。

最后一步,当 HMR 失败后,回退到 live reload 操作,也就是进行浏览器刷新来获取最新打包代码。

  1. 如何利用 webpack 来优化前端性能?(提高性能和体验)

用 webpack 优化前端性能是指优化 webpack 的输出结果,让打包的最终结果在浏览器运行快速高效。

压缩代码。删除多余的代码、注释、简化代码的写法等等方式。可以利用 webpack 的 UglifyJsPlugin 和 ParallelUglifyPlugin 来压缩 JS 文件, 利用 cssnano(css-loader?minimize)来压缩 css

利用 CDN 加速。在构建过程中,将引用的静态资源路径修改为 CDN 上对应的路径。可以利用 webpack 对于 output 参数和各 loader 的 publicPath 参数来修改资源路径

删除死代码(Tree Shaking)。将代码中永远不会走到的片段删除掉。可以通过在启动 webpack 时追加参数–optimize-minimize 来实现

提取公共代码。

  1. 如何提高 webpack 的构建速度?

多入口情况下,使用 CommonsChunkPlugin 来提取公共代码

通过 externals 配置来提取常用库

利用 DllPlugin 和 DllReferencePlugin 预编译资源模块 通过 DllPlugin 来对那些我们引用但是绝对不会修改的 npm 包来进行预编译,再通过 DllReferencePlugin 将预编译的模块加载进来。

使用 Happypack 实现多线程加速编译

使用 webpack-uglify-parallel 来提升 uglifyPlugin 的压缩速度。 原理上 webpack-uglify-parallel 采用了多核并行压缩来提升压缩速度

使用 Tree-shaking 和 Scope Hoisting 来剔除多余代码

  1. 怎么配置单页应用?怎么配置多页应用?

单页应用可以理解为 webpack 的标准模式,直接在 entry 中指定单页应用的入口即可,这里不再赘述

多页应用的话,可以使用 webpack 的 AutoWebPlugin 来完成简单自动化的构建,但是前提是项目的目录结构必须遵守他预设的规范。 多页应用中要注意的是:

每个页面都有公共的代码,可以将这些代码抽离出来,避免重复的加载。比如,每个页面都引用了同一套 css 样式表

随着业务的不断扩展,页面可能会不断的追加,所以一定要让入口的配置足够灵活,避免每次添加新页面还需要修改构建配置

  1. npm 打包时需要注意哪些?如何利用 webpack 来更好的构建?

Npm 是目前最大的 JavaScript 模块仓库,里面有来自全世界开发者上传的可复用模块。你可能只是 JS 模块的使用者,但是有些情况你也会去选择上传自己开发的模块。 关于 NPM 模块上传的方法可以去官网上进行学习,这里只讲解如何利用 webpack 来构建。

NPM 模块需要注意以下问题:

要支持 CommonJS 模块化规范,所以要求打包后的最后结果也遵守该规则。

Npm 模块使用者的环境是不确定的,很有可能并不支持 ES6,所以打包的最后结果应该是采用 ES5 编写的。并且如果 ES5 是经过转换的,请最好连同 SourceMap 一同上传。

Npm 包大小应该是尽量小(有些仓库会限制包大小)

发布的模块不能将依赖的模块也一同打包,应该让用户选择性的去自行安装。这样可以避免模块应用者再次打包时出现底层模块被重复打包的情况。

UI 组件类的模块应该将依赖的其它资源文件,例如.css 文件也需要包含在发布的模块里。

基于以上需要注意的问题,我们可以对于 webpack 配置做以下扩展和优化:

CommonJS 模块化规范的解决方案:

设置output.libraryTarget='commonjs2'使输出的代码符合 CommonJS2 模块化规范,以供给其它模块导入使用

输出 ES5 代码的解决方案:

使用 babel-loader 把 ES6 代码转换成 ES5 的代码。再通过开启devtool: 'source-map'输出 SourceMap 以发布调试。

Npm 包大小尽量小的解决方案:

Babel 在把 ES6 代码转换成 ES5 代码时会注入一些辅助函数,最终导致每个输出的文件中都包含这段辅助函数的代码,造成了代码的冗余。解决方法是修改.babelrc 文件,为其加入 transform-runtime 插件

不能将依赖模块打包到 NPM 模块中的解决方案:

使用 externals 配置项来告诉 webpack 哪些模块不需要打包。

对于依赖的资源文件打包的解决方案:通过 css-loader 和 extract-text-webpack-plugin 来实现,配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const ExtractTextPlugin = require("extract-text-webpack-plugin");

module.exports = {
module: {
rules: [
{
//增加对CSS文件的支持
test: /\.css/,
//提取出 Chunk 中的CSS代码到单独的文件中
use: ExtractTextPlugin.extract({
use: ["css-loader"],
}),
},
],
},
plugins: [
new ExtractTextPlugin({
//输出的CSS文件名称
filename: "index.css",
}),
],
};
  1. 如何在 vue 项目中实现按需加载?

Vue UI 组件库的按需加载:

为了快速开发前端项目,经常会引入现成的 UI 组件库如 ElementUI、iView 等,但是他们的体积和他们所提供的功能一样,是很庞大的。 而通常情况下,我们仅仅需要少量的几个组件就足够了,但是我们却将庞大的组件库打包到我们的源码中,造成了不必要的开销。

不过很多组件库已经提供了现成的解决方案,如 Element 出品的 babel-plugin-component 和 AntDesign 出品的 babel-plugin-import 安装以上插件后,在.babelrc 配置中或 babel-loader 的参数中进行设置,即可实现组件按需加载了。

单页应用的按需加载

现在很多前端项目都是通过单页应用的方式开发的,但是随着业务的不断扩展,会面临一个严峻的问题——首次加载的代码量会越来越多,影响用户的体验。

通过import(*)语句来控制加载时机,webpack 内置了对于import(*)的解析,会将import(*)中引入的模块作为一个新的入口在生成一个 chunk。 当代码执行到import(*)语句时,会去加载 Chunk 对应生成的文件。import()会返回一个Promise 对象,所以为了让浏览器支持,需要事先注入Promise polyfill.

Nginx

nginx 是一个反向代理服务器.客户端向服务端发送请求,可以配置一个 nginx 代理服务器.

反向代理

代理服务器负责接收请求,转发给内部的服务器.从颞部服务器得到响应返回给客户端.

反向代理服务器为服务端工作.对服务端是透明的,对客户端不透明.

nginx 的作用

解决跨域

请求过滤

负载均衡

配置 gzip

静态资源服务器

解决跨域

在 server 中配置一个 server-name,和客户端同域名

在 Location 中拦截向客户端域名发送的请求,并且请求代理回服务器.

请求过滤

在 Location 中过滤相应的状态码或者 url,或者请求类型

负载均衡

通过配置 upstream 指定后端服务器的地址列表

在 server 中拦截请求,并将请求转发到 upstream 的后端服务器列表里.

策略

默认采用轮训的策略,缺点是一台服务器压力过大时,会影响其他服务器.

最小连接数策略,优先分配给连接数最少的服务器

least_connt

最快响应时间策略,优先分配给响应时间最短的服务器

fair

静态资源服务器

在 Location 中配置,匹配比如说图片为结尾的请求,转发到本地路径.设置 root 指定路径.


title: Vue组件

date: 2019-09-09 11:23:52

tags:  # 这里写的分类会自动汇集到 categories 页面上,分类可以多级

  • Vue.js # 一级分类
  • Vue.js基础 # 二级分类

作用

提高代码复用性

组件使用方法

全局注册

全局注册时,Vue.component需要在new Vue实例之前注册.否则报错.

1
2
3
4
5
6
7
8
9
Vue.component('button-counter',{
data: function(){
return {
count: 0
}
},
template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
})
})

优点:所有的 vue 实例都可以用

缺点:权限太大,容错率降低

局部注册

1
2
3
4
5
6
7
8
var app = new Vue({
el: "#app",
components: {
"my-component": {
template: "<div>我是组件的内容</div>",
},
},
});

注意局部注册的组件在其子组件相互之间是不可用的。

如果你希望 ComponentA 在 ComponentB 中可用,则你需要这样写:

1
2
3
4
5
6
7
8
9
10
var ComponentA = {
/* ... */
};

var ComponentB = {
components: {
"component-a": ComponentA,
},
// ...
};

或者采用 import 方法:

1
2
3
4
5
6
7
8
import ComponentA from "./ComponentA.vue";

export default {
components: {
ComponentA,
},
// ...
};

组件的复用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<div id="components-demo">
<button-counter></button-counter>
<button-counter></button-counter>
<button-counter></button-counter>
</div>

<script>
Vue.component('button-counter',{
data: function(){
return {
count: 0
}
},
template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
})
})
</script>

问题: 点击按钮时,三个按键的数字都会变化吗?为什么?

注意当点击按钮时,每个组件都会各自独立维护它的count。因为你每用一次组件,就会有一个它的新实例被创建。

特殊情况

vue 组件的模板在某些情况下会受到 html 标签的限制,比如 <table> 中只能还 有 <tr> , <td>这些元素,所以直接在 table 中使用组件是无效的,此时可以使用is属性来挂载组件

1
2
3
<table>
<tbody is="my-component"></tbody>
</table>

组件使用技巧

  1. 必须使用小写字母加 ­ 进行命名child,my­component命名组件
  2. template 中的内容必须被一个 DOM 元素包括,也可以嵌套
  3. 在组件的定义中,除了 template 之外还可以使用其他选项,比如data,computed,methods
  4. 一个组件的 data 选项必须是一个函数,否则就会出现点一个其他按钮也跟着变化的情况

组件中 data 什么时候可以用对象

因为组件内 data 会复用。一个组件修改,就都会改

解决办法:

new Vue(),生成一个根实例。该组件不会复用,也就不会共享。

组件通信

通过 prop 父传子通信

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<div id="blog-post-demo">
<h5>我是父组件</h5>
<child-post
msg="我是来自父组件的内容"
v-for="post in posts"
v-bind:key="post.id"
v-bind:title="post.title"
>
</child-post>
</div>

<script>
Vue.component('child-post',{
props: ['title','msg'],
template: '<h3>{{ title }} + {{msg}}</h3>'
})

new Vue({
el: "#blog-post-demo",
data: {
posts: [
{id: 1, title: 'aaa' },
{id: 2, title: 'bbb' },
{id: 3, title: 'ccc' }
]
}
}
)

</script>

一个组件默认可以拥有任意数量的prop,任何值都可以传递给任何prop。在上述模板中,你会发现我们能够在组件实例中访问这个值,就像访问data中的值一样。

如上所示,你会发现我们可以使用v-bind来动态传递prop。这在你一开始不清楚要渲染的具体内容,比如从一个 API 获取博文列表的时候,是非常有用的。

总结

  1. 在组件中使用props来从父亲组件接收参数,注意,在props中定义的属性,都可以在组件中直接使用
  2. props 来自父级,而组件中 data return 的数据就是组件自己的数据,两种情况作用域就是 组件本身,可以在 template,computed,methods 中直接使用
  3. props 的值有两种,一种是字符串数组,一种是对象,本节先只讲数组
  4. 可以使用 v-­bind 动态绑定父组件来的内容

单向数据流

  • 解释:  通过props传递数据是单向的了,也就是父组件数据变化时会传递给子组件,但是反过来不行。
  • 目的: 是尽可能将父子组件解耦,避免子组件无意中修改了父组件的状态。
  • 应用场景: 业务中会经常遇到两种需要改变prop的情况

一种是父组件传递初始值进来,子组件将它作为初始值保存起来,在自己的作用域下可以随意使用和修改。这种情况可以在组件 data 内再声明一个数据,引用父组件的prop

步骤一:注册组件

步骤二:将父组件的数据传递进来,并在子组件中用 props 接收

步骤三:将传递进来的数据通过初始值保存起来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<div id="app">
<my-comp init-count="666"></my-comp>
</div>
<script>
var app = new Vue({
el: '#app',
components: {
'my-comp': {
props: ['init-count'],
template: '<div>{{init-count}}</div>',
data: function() {
return {
//初始值count.props中的值通过this.XXX获取
count: this.initCount
}
}
}
}
})
</script>

另一种情况就是prop作为需要被转变的原始值传入。这种情况用计算属性就可以了

步骤一:注册组件

步骤二:将父组件的数据传递进来,并在子组件中用 props 接收

步骤三:将传递进来的数据通过计算属性进行重新计算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<div>
<input type="text" v-model="width">
<my-comp :width="width"></my-comp>
</div>

<script>
var app = new Vue({
el: "#app",
data: {
width: ''
},
components: {
'my-comp': {
props: ['init-count', 'width'],
template: '<div :style="style">{{ init-count }}</div>',
computed: {
style: function(){
return {
width: this.width + 'px',
background: 'red'
}
}
}
}
}
})
</script>

驼峰命名与短横线命名

  1. 在 html 中,myMessagemymessage是一样的.因此在组件中的 html 中必须使用短横线命名.
  2. 在组件中,父组件给子组件传数据必须用短横线(属于 HTML 范围内),因为 html 不识别驼峰,在props中无所谓.在template中必须使用驼峰命名.
  3. 在组件中的data中,用this.XXX引用时,必须使用驼峰命名.

数据验证

验证的  type  类型可以是:

• String

• Number

• Boolean

• Object

• Array

• Function

在 props 中设定数据类型时,必须使用对象格式,即

1
2
3
4
5
6
7
8
9
10
11
12
props: {
//msg必须是数字
msg: Number,
//total既可以是数字又可以是字符串
total: [Number,String]
//ach是布尔值,默认是true,required是必传项.
ach: {
type: Boolean,
default: true,
required: true
}
}

组件通信

组件通信分为父子通信,兄弟通信,跨级通信.

子传父通信

使用v­-on除了监听 DOM 事件外,还可以用于组件之间的自定义事件.

JavaScript 的设计模式 一一观察者模式, dispatchEventaddEventListener这两个方法.Vue 组件也有与之类似的一套模式,

子组件用$emit()来触发事件,父组件用$on()来监听子组件的事件.

  1. 自定义事件change
  2. 在子组件中用$emit触发change,即this.$emit('change',this.count).括号里前面是用户名,后面是传递参数.
  3. 在自定义事件中用一个参数来接受
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<div id="app">
<p>您的余额为{{ total }}</p>
//父组件内用v-on监听change事件
<btn-component @change="handleTotal"></btn-component>
</div>

<script>
var app = new Vue({
el: "#app",
data: {
total: 0
},
methods: {
handleTotal: function(value){
this.total = value
}
},
components: {
'btn-component': {
template: '<div>\
<button @click="handleIncrease">+1</button>\
<button @click="handleReduce">-1</button>\
</div>',
data: function(){
return {
count: 0
}
},
methods: {
handleIncrease: function(){
this.count++;
this.$emit('change', this.count)
},
handleReduce: function(){
this.count--
this.$emit('change', this.count)
}
}
}
}

})

</script>

在组件中使用 v-model

$emit的代码实际上会触发一个 input 事件,input后的参数就是传递给v-model绑定的属性的值.

v-model其实是一个语法糖,其实绑定了两层操作:

  1. v-bind绑定一个 value 值
  2. v-on指令给当前元素绑定 input 事件
1
2
3
4
5
6
<input v-model="total">
//等价于
<input
v-bind:value="total"
v-on:input="total = $event.target.value"
>

要使用v-model,要做到:

  1. 接收一个 value 属性
  2. 在有新的 value 时触发 input 事件

修改上面的实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<div id="app">
<p>您的余额为{{ total }}</p>
<btn-component v-model="total"></btn-component>
//如果又添加一个相同的组件,点击也会变化,只不过显示的值属于各自的结果,不冲突
<btn-component v-model="total"></btn-component>
</div>
<script>
var app = new Vue({
el: "#app",
data: {
total: 0
},
components: {
'btn-component': {
template: `<div>
<button @click="handleincrease">+1</button>
<button @click="handlereduce">-1</button>
</div>`,
data: function(){
return {
count: 0
}
},
methods: {
handleincrease: function(){
this.count++
this.$emit('input',this.count)
},
handlereduce: function(){
this.count--
this.$emit('input',this.count)
}
}
}
}
})
</script>

V-model 简单实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<div id="app">
<h5>我是父组件</h5>
<input type="text" v-model="parentmsg">
// 下面的是子组件,通过v-bind绑定msg到父组件中v-model所绑定的parentmsg,通过props传递到模板中渲染到页面
<child :msg="parentmsg"></child>
</div>

<script>
Vue.component('child',{
props: ['msg'],
template: '<h3>{{msg}}</h3>'
})

new Vue({
el: "#app",
data: {
parentmsg:''
}
}

)
</script>

非父子组件通信(兄弟通信)

方法: 使用 bus 中介

使用一个空的 Vue 实例作为中央事件总线(bus):

var bus = new Vue()

触发组件 A 中的事件

bus.$emit('id-selected',1)

在组件 B 创建的钩子中监听事件

bus.$on('id-selected',function(id){//....})

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<div id="app">
<my-acomponent></my-acomponent>
<my-bcomponent></my-bcomponent>
</div>

<script>
Vue.component('my-acomponent',{
template: `<div><button @click="handle">点击我向B组件传数据</button></div>`,
data: function(){
return {
aaa: '我来自a组件'
}
},
methods: {
handle: function(){
this.$root.bus.$emit('lala', this.aaa)
}
}
})

Vue.component('my-bcomponent',{
template: `<div>我是B组件</div>`,
created: function(){
//A组件在实例创建的时候就监听事件--lala事件
this.$root.bus.$on('lala',function(value){
alert(value)
})
}
})

var app = new Vue({
el:"#app",
data: {
//bus中介
bus: new Vue()
}
})
</script>

父链和子链

this.$parent(从父组件里拿内容)

1
2
3
4
5
6
7
8
Vue.component("child-component", {
template: `<button @click="setFatherData">通过我修改父亲的数据</button>`,
methods: {
setFatherData: function () {
this.$parent.msg = "数据已修改";
},
},
});

this$refs(从子组件里拿内容 )

为子组件提供索引的方法,用特殊的属性 ref 为其增加一个索引

如果用$children会把所有子组件都拿到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<div id="app">
<my-acomponent ref="A"></my-acomponent>
<my-bcomponent ref="B"></my-bcomponent>
<button @click="getChildData">我是父组件,我要拿到子组件的数据</button>---{{ msg }}
</div>
<script>
var app = new Vue({
el: "#app",
data: {
bus: new Vue()
msg: '数据未拿到',
formchild: '还未拿到'
},
methods: {
getChildData: function(){
//用来拿子组件中的内容---$refs
this.fromChild = this.$refs.b.msg
}
}

})

Vue.component('my-acomponent',{
template: `<div></div>`,
data: function(){
return {
msg: '我来自a组件'
}
}
})
</script>

使用 slot 插槽分发内容

什么是插槽

为了让组件可以组合,我们需要一种方式来混合父组件的内容与子组件自己的模板。这个过程被称为内容分发.Vue.js  实现了一个内容分发 API,使用特殊的slot元素作为原始内容的插槽.

编译的作用域

父组件的作用域在父组件内,即<div id="app"></div>内部(便于理解).

子组件的作用域在子组件的template里.

插槽的用法

混合父组件的内容和子组件的模板

  1. 单个插槽
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<div id="app">
<my-component>
<p>父组件</p>
//虽然是父组件的作用域,但没有slot插槽,父组件的信息是无法显示
//插槽的作用是把父组件的内容插入到下面的子组件中,最后会显示出来
</my-component>
</div>
<script>
Vue.component('my-component',{
template: `<div>
<slot>
如果父组件没有插入内容,那么我就作为默认出现
</slot>
</div>`
})
var app = new Vue({
el:"#app",
data: {

}
})
</script>
  1. 具名插槽
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<div id="app">
<my-component>
<h3 slot="header">我是标题</h3>
//给父组件的插槽命名,可以和子组件模板的slot一一对应
<p>父组件</p>
//这个不命名的自然就对应那个没有命名的slot
<p slot="footer">我是底部信息</p>
</my-component>
</div>
<script>
Vue.component('my-component',{
template: `<div>
<div class="header">
<slot name="header"></slot>
</div>
<div class="container">
<slot></slot>
</div>
<div class="footer">
<slot name="footer"></slot>
</div>
</div>`
})
var app = new Vue({
el:"#app",
data: {

}
})
</script>
  1. 作用域插槽

作用域插槽是一种特殊的插槽,使用一个可复用的模板来替换已渲染的元素

  • 从子组件获取数据
  • <template>标签是不会渲染出来的,Vue 版本更新后,也可以写在其他标签上
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<div id="app">
<my-component>
<template slot="abc" slot-scope="prop">
//写一个临时变量prop,用临时变量拿子组件的信息
{{ prop.text }}
</template>
</my-component>
</div>
<script>
Vue.component('my-component',{
template: `<div>
<slot name="abc" text="我是子组件的内容">
</slot>
</div>`
})
var app = new Vue({
el:"#app",
data: {

}
})
</script>

访问slot

通过this.$slots.(name)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<div id="app">
<my-component>
<h3 slot="header">我是标题</h3>
<p>父组件</p>
<p slot="footer">我是底部信息</p>
</my-component>
</div>
<script>
Vue.component('my-component',{
template: `<div>
<div class="header">
<slot name="header"></slot>
</div>
<div class="container">
<slot></slot>
</div>
<div class="footer">
<slot name="footer"></slot>
</div>
</div>`,
mounted: function(){
//访问插槽
var header = this.$slots.header
var text = header[0].elm.innerText
console.log(header)
//打印一个虚拟节点
console.log(text)
}
})
var app = new Vue({
el:"#app",
data: {

}
})
</script>

组件高级用法-动态组件

实现需求: 点击不同按钮切换不同页面

使用is动态绑定组件,调用方法切换不同页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<div id="app">
<component :is="thisView"></component>
<button @click="handleView('A')">第一句</button>
<button @click="handleView('B')">第二句</button>
<button @click="handleView('C')">第三句</button>
<button @click="handleView('D')">第四句</button>
</div>
<script>
Vue.component('comA',{
template:`<div>鹅鹅鹅</div>`
})
Vue.component('comB',{
template:`<div>曲项向天歌</div>`
})
Vue.component('comC',{
template:`<div>白毛浮绿水</div>`
})
Vue.component('comD',{
template:`<div>红掌拨清波</div>`
})

var app = new Vue({
el: "#app",
data: {
thisView: 'comA'
},
methods: {
handleView: function(tag){
this.thisView = 'com' + tag
}
}
})
</script>

解析 DOM 模板时的注意事项

有些 HTML 元素,诸如<ul><ol><table><select>,对于哪些元素可以出现在其内部是有严格限制的。而有些元素,诸如 <li><tr><option>,只能出现在其它某些特定的元素内部。

1
2
3
<table>
<blog-post-row></blog-post-row>
</table>

这个自定义组件 <blog-post-row> 会被作为无效的内容提升到外部,并导致最终渲染结果出错。幸好这个特殊的is特性给了我们一个变通的办法:

1
2
3
<table>
<tr is="blog-post-row"></tr>
</table>

需要注意的是如果我们从以下来源使用模板的话,这条限制是不存在的:

  1. 字符串 (例如:template: ‘…’)
  2. 单文件组件 (.vue)
  3. <script type="text/x-template">

模块系统

在模块系统中局部注册

推荐创建一个 components 目录,并将每个组件放置在其各自的文件中。

然后你需要在局部注册之前导入每个你想使用的组件。例如,在一个假设的 ComponentB.jsComponentB.vue 文件中:

1
2
3
4
5
6
7
8
9
10
import ComponentA from "./ComponentA";
import ComponentC from "./ComponentC";

export default {
components: {
ComponentA,
ComponentC,
},
// ...
};

现在 ComponentA 和 ComponentC 都可以在 ComponentB 的模板中使用了

基础组件的自动化全局注册

可能你的许多组件只是包裹了一个输入框或按钮之类的元素,是相对通用的。我们有时候会把它们称为基础组件,它们会在各个组件中被频繁的用到。

所以会导致很多组件里都会有一个包含基础组件的长列表:

1
2
3
4
5
6
7
8
9
10
11
import BaseButton from "./BaseButton.vue";
import BaseIcon from "./BaseIcon.vue";
import BaseInput from "./BaseInput.vue";

export default {
components: {
BaseButton,
BaseIcon,
BaseInput,
},
};

如果你使用了 webpack (或在内部使用了 webpack 的 Vue CLI 3+),那么就可以使用 require.context 只全局注册这些非常通用的基础组件。这里有一份可以让你在应用入口文件 (比如 src/main.js) 中全局导入基础组件的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import Vue from "vue";
import upperFirst from "lodash/upperFirst";
import camelCase from "lodash/camelCase";

const requireComponent = require.context(
// 其组件目录的相对路径
"./components",
// 是否查询其子目录
false,
// 匹配基础组件文件名的正则表达式
/Base[A-Z]\w+\.(vue|js)$/
);

requireComponent.keys().forEach((fileName) => {
// 获取组件配置
const componentConfig = requireComponent(fileName);

// 获取组件的 PascalCase 命名
const componentName = upperFirst(
camelCase(
// 获取和目录深度无关的文件名
fileName
.split("/")
.pop()
.replace(/\.\w+$/, "")
)
);

// 全局注册组件
Vue.component(
componentName,
// 如果这个组件选项是通过 `export default` 导出的,
// 那么就会优先使用 `.default`,
// 否则回退到使用模块的根。
componentConfig.default || componentConfig
);
});

记住全局注册的行为必须在根 Vue 实例 (通过 new Vue) 创建之前发生.

Prop

Prop 的大小写

HTML 中的特性名是大小写不敏感的,所以浏览器会把所有大写字符解释为小写字符.

当使用 dom 中的模板时,驼峰命名的 prop 需要使用其等价的短横线命名替换:

1
2
3
4
5
6
7
8
9
10
//js
Vue.component('blog-post', {
// 在 JavaScript 中是 驼峰 的
props: ['postTitle'],
template: '<h3>{{ postTitle }}</h3>'
})

//html
<!-- 在 HTML 中是短横线的 -->
<blog-post post-title="hello!"></blog-post>

prop 的类型

字符串和对象

字符串:props: ['title', 'likes', 'isPublished', 'commentIds', 'author']

对象:

1
2
3
4
5
6
7
8
9
props: {
title: String,
likes: Number,
isPublished: Boolean,
commentIds: Array,
author: Object,
callback: Function,
contactsPromise: Promise // or any other constructor
}

传递静态或动态 Prop

传静态值:

1
<blog-post title="My journey with Vue"></blog-post>

动态赋值:

1
<blog-post v-bind:title="post.title"></blog-post>

布尔值:

1
2
3
4
5
6
7
8
9
<!-- 包含该 prop 没有值的情况在内,都意味着 `true`。-->
<blog-post is-published></blog-post>

<!-- 即便 `false` 是静态的,我们仍然需要 `v-bind` 来告诉 Vue -->
<!-- 这是一个 JavaScript 表达式而不是一个字符串。-->
<blog-post v-bind:is-published="false"></blog-post>

<!-- 用一个变量进行动态赋值。-->
<blog-post v-bind:is-published="post.isPublished"></blog-post>

数组

1
2
3
4
5
6
<!-- 即便数组是静态的,我们仍然需要 `v-bind` 来告诉 Vue -->
<!-- 这是一个 JavaScript 表达式而不是一个字符串。-->
<blog-post v-bind:comment-ids="[234, 266, 273]"></blog-post>

<!-- 用一个变量进行动态赋值。-->
<blog-post v-bind:comment-ids="post.commentIds"></blog-post>

对象

1
2
3
4
5
6
7
8
9
10
11
<!-- 即便对象是静态的,我们仍然需要 `v-bind` 来告诉 Vue -->
<!-- 这是一个 JavaScript 表达式而不是一个字符串。-->
<blog-post
v-bind:author="{
name: 'Veronica',
company: 'Veridian Dynamics'
}"
></blog-post>

<!-- 用一个变量进行动态赋值。-->
<blog-post v-bind:author="post.author"></blog-post>

禁用特性继承

如果你不希望组件的根元素继承特性,你可以在组件的选项中设置 inheritAttrs: false。例如:

1
2
3
4
Vue.component("my-component", {
inheritAttrs: false,
// ...
});

这尤其适合配合实例的$attrs 属性

1
2
3
4
{
required: true,
placeholder: 'Enter your username'
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Vue.component("base-input", {
inheritAttrs: false,
props: ["label", "value"],
template: `
<label>
{{ label }}
<input
v-bind="$attrs"
v-bind:value="value"
v-on:input="$emit('input', $event.target.value)"
>
</label>
`,
});
1
2
3
4
5
6
//html
<base-input
v-model="username"
required
placeholder="Enter your username"
></base-input>

注意 inheritAttrs: false 选项不会影响 style 和 class 的绑定。

自定义事件

事件名

不同于组件和 prop,事件名不会被用作一个 js 变量名或属性名,所以就没有理由使用驼峰命名了。并且v-on事件监听器在 DOM 模板中会被自动转换为全小写 (因为 HTML 是大小写不敏感的),所以推荐始终使用短横线命名.

自定义组件的 v-model

一个组件上的 v-model 默认会利用名为 value 的 prop 和名为 input 的事件,但是像单选框、复选框等类型的输入控件可能会将 value 特性用于不同的目的。model 选项可以用来避免这样的冲突:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Vue.component("base-checkbox", {
model: {
prop: "checked",
event: "change",
},
props: {
checked: Boolean,
},
template: `
<input
type="checkbox"
v-bind:checked="checked"
v-on:change="$emit('change', $event.target.checked)"
>
`,
});

现在在这个组件上使用 v-model 的时候:

1
<base-checkbox v-model="lovingVue"></base-checkbox>

这里的 lovingVue 的值将会传入这个名为 checked 的 prop。同时当 <base-checkbox>触发一个 change 事件并附带一个新的值的时候,这个 lovingVue 的属性将会被更新。

注意你仍然需要在组件的 props 选项里声明 checked 这个 prop。

将原生事件绑定到组件

使用v-on的修饰符.native直接监听一个组件根元素上的原生事件

1
<base-input v-on:focus.native="onFocus"></base-input>

但当上述 <base-input> 组件做了如下重构,那么根元素实际上是一个 <label> 元素:

1
2
3
4
5
6
7
8
<label>
{{ label }}
<input
v-bind="$attrs"
v-bind:value="value"
v-on:input="$emit('input', $event.target.value)"
>
</label>

这时,父级的 .native 监听器将静默失败。它不会产生任何报错,但是 onFocus 处理函数不会如你预期地被调用。

为了解决这个问题,Vue 提供了一个 $listeners 属性,它是一个对象,里面包含了作用在这个组件上的所有监听器。例如:

1
2
3
4
{
focus: function (event) { /* ... */ }
input: function (value) { /* ... */ },
}

$listeners

有了这个 listeners” 将所有的事件监听器指向这个组件的某个特定的子元素。对于类似 <input> 的你希望它也可以配合 v-model 工作的组件来说,为这些监听器创建一个类似下述 inputListeners 的计算属性通常是非常有用的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
Vue.component("base-input", {
inheritAttrs: false,
props: ["label", "value"],
computed: {
inputListeners: function () {
var vm = this;
// `Object.assign` 将所有的对象合并为一个新对象
return Object.assign(
{},
// 我们从父级添加所有的监听器
this.$listeners,
// 然后我们添加自定义监听器,
// 或覆写一些监听器的行为
{
// 这里确保组件配合 `v-model` 的工作
input: function (event) {
vm.$emit("input", event.target.value);
},
}
);
},
},
template: `
<label>
{{ label }}
<input
v-bind="$attrs"
v-bind:value="value"
v-on="inputListeners"
>
</label>
`,
});

现在<base-input>组件是一个完全透明的包裹器了,也就是说它可以完全像一个普通的 <input>元素一样使用了:所有跟它相同的特性和监听器的都可以工作。

.sync 修饰符

在有些情况下,我们可能需要对一个 prop 进行“双向绑定”。不幸的是,真正的双向绑定会带来维护上的问题,因为子组件可以修改父组件,且在父组件和子组件都没有明显的改动来源。

这也是为什么我们推荐以 update:myPropName 的模式触发事件取而代之。举个例子,在一个包含 title prop 的假设的组件中,我们可以用以下方法表达对其赋新值的意图:

1
this.$emit("update:title", newTitle);

然后父组件可以监听那个事件并根据需要更新一个本地的数据属性。例如:

1
2
3
4
<text-document
v-bind:title="doc.title"
v-on:update:title="doc.title = $event"
></text-document>

为了方便起见,我们为这种模式提供一个缩写,即 .sync 修饰符:

1
<text-document v-bind:title.sync="doc.title"></text-document>

注意带有 .sync 修饰符的 v-bind 不能和表达式一起使用 (例如 v-bind:title.sync=”doc.title + ‘!’” 是无效的)。取而代之的是,你只能提供你想要绑定的属性名,类似 v-model。

当我们用一个对象同时设置多个 prop 的时候,也可以将这个 .sync 修饰符和 v-bind 配合使用:

1
<text-document v-bind.sync="doc"></text-document>

这样会把 doc 对象中的每一个属性 (如 title) 都作为一个独立的 prop 传进去,然后各自添加用于更新的 v-on 监听器。

v-bind.sync 用在一个字面量的对象上,例如 v-bind.sync="{ title: doc.title }",是无法正常工作的,因为在解析一个像这样的复杂表达式的时候,有很多边缘情况需要考虑。

动态组件和异步组件

动态组件 is

使用is特性切换不同的组件,如果想在标签的组件实例被在它们第一次被创建的时候缓存下来,避免重复渲染.可以使用<keep-alive> 元素将其动态组件包裹起来.

1
2
3
4
<!-- 失活的组件将会被缓存!-->
<keep-alive>
<component v-bind:is="currentTabComponent"></component>
</keep-alive>

异步组件

在大型应用中,我们可能需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载一个模块。为了简化,Vue 允许你以一个工厂函数的方式定义你的组件,这个工厂函数会异步解析你的组件定义。Vue 只有在这个组件需要被渲染的时候才会触发该工厂函数,且会把结果缓存起来供未来重渲染。

异步组件的 3 种实现方式—工厂函数、Promise、高级函数

异步组件实现的本质是 2 次渲染,先渲染成注释节点,当组件加载成功后,在通过 forceRender 重新渲染

高级异步组件可以通过简单的配置实现 loading   resolve   reject   timeout  4 种状态.

例如:

1
2
3
4
5
6
7
8
Vue.component("async-example", function (resolve, reject) {
setTimeout(function () {
// 向 `resolve` 回调传递组件定义
resolve({
template: "<div>I am async!</div>",
});
}, 1000);
});

函数有 2 个参数,resolve 和 reject,它们是两个函数,由 javascript 引擎提供,不用自己定义。resolve 会在你从服务器得到组件定义的时候被调用。

如你所见,这个工厂函数会收到一个 resolve回调,这个回调函数会在你从服务器得到组件定义的时候被调用。你也可以调用 reject(reason) 来表示加载失败。这里的 setTimeout 是为了演示用的,如何获取组件取决于你自己.

一个推荐的做法是将异步组件和 webpack 的 code-splitting 功能一起配合使用:

1
2
3
4
5
6
7
8
Vue.component("async-webpack-example", function (resolve) {
// 这个特殊的 `require` 语法将会告诉 webpack
// 自动将你的构建代码切割成多个包,这些包
// 会通过 Ajax 请求加载
require(["./my-async-component"], resolve);
});
//这里使用的是webpack模块方法require(AMD版本),将其对应的文件拆分到一个单独的 bundle 中,
//此 bundle 会被异步加载,然后调用resolve回调函数

你也可以在工厂函数中返回一个 Promise,所以把 webpack 2 和 ES2015 语法加在一起,我们可以写成这样:

1
2
3
4
5
6
Vue.component(
"async-webpack-example",
// 这个 `import` 函数会返回一个 `Promise` 对象。
() => import("./my-async-component")
);
//这里使用的webpack模块方法import(),可以通过注释的方法定义新chunk的名称

处理加载状态

这里的异步组件工厂函数也可以返回一个如下格式的对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
const AsyncComponent = () => ({
// 需要加载的组件 (应该是一个 `Promise` 对象)
component: import("./MyComponent.vue"),
// 异步组件加载时使用的组件
loading: LoadingComponent,
// 加载失败时使用的组件
error: ErrorComponent,
// 展示加载时组件的延时时间。默认值是 200 (毫秒)
delay: 200,
// 如果提供了超时时间且组件加载也超时了,
// 则使用加载失败时使用的组件。默认值是:`Infinity`
timeout: 3000,
});

当使用局部注册的时候,也可以直接提供一个返回 promise 的函数,比如在使用 vue 的路由懒加载时:

1
2
3
4
//懒加载方式,当路由被访问时才加载对应组件
const Login = () => import("@/components/Login");
const Home = (resolve) => require(["@/components/Home"], resolve);
const UserList = (resolve) => require(["@/components/user/list"], resolve);

这样只有当要访问路由时,才会加载指定路由下的组件.

处理边界情况

访问元素和组件

访问根实例

在每个new Vue实例的子组件中,其根实例可以通过$root属性进行访问.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Vue 根实例
new Vue({
data: {
foo: 1,
},
computed: {
bar: function () {
/* ... */
},
},
methods: {
baz: function () {
/* ... */
},
},
});

// 获取根组件的数据
this.$root.foo;

// 写入根组件的数据
this.$root.foo = 2;

// 访问根组件的计算属性
this.$root.bar;

// 调用根组件的方法
this.$root.baz();

访问父组件实例

$root类似,$parent 属性可以用来从一个子组件访问父组件的实例。它提供了一种机会,可以在后期随时触达父级组件,以替代将数据以 prop 的方式传入子组件的方式。

访问子组件实例

通过$ref

依赖注入

依赖注入用到了两个新的实例选项:provideinject

provide 选项允许我们指定我们想要提供给后代组件的数据/方法。在这个例子中,就是 <google-map> 内部的 getMap 方法

1
2
3
4
5
provide: function () {
return {
getMap: this.getMap
}
}

然后在任何后代组件里,我们都可以使用 inject 选项来接收指定的我们想要添加在这个实例上的属性:

1
inject: ["getMap"];

相比$parent来说,这个用法可以让我们在任意后代组件中访问 getMap,而不需要暴露整个 <google-map> 实例。这允许我们更好的持续研发该组件,而不需要担心我们可能会改变/移除一些子组件依赖的东西。同时这些组件之间的接口是始终明确定义的,就和 props 一样。

实际上,你可以把依赖注入看作一部分“大范围有效的 prop”,除了:

  1. 祖先组件不需要知道哪些后代组件使用它提供的属性
  2. 后代组件不需要知道被注入的属性来自哪里

然而,依赖注入还是有负面影响的。它将你应用程序中的组件与它们当前的组织方式耦合起来,使重构变得更加困难。同时所提供的属性是非响应式的。这是出于设计的考虑,因为使用它们来创建一个中心化规模化的数据跟使用 $root做这件事都是不够好的。如果你想要共享的这个属性是你的应用特有的,而不是通用化的,或者如果你想在祖先组件中更新所提供的数据,那么这意味着你可能需要换用一个像 Vuex 这样真正的状态管理方案了。

程序化的事件侦听器

现在,你已经知道了 $emit 的用法,它可以被 v-on 侦听,但是 Vue 实例同时在其事件接口中提供了其它的方法。我们可以:

  1. 通过 $on(eventName, eventHandler) 侦听一个事件
  2. 通过 $once(eventName, eventHandler) 一次性侦听一个事件
  3. 通过 $off(eventName, eventHandler) 停止侦听一个事件

让多个输入框元素同时使用不同的 Pikaday,每个新的实例都程序化地在后期清理它自己:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mounted: function () {
this.attachDatepicker('startDateInput')
this.attachDatepicker('endDateInput')
},
methods: {
attachDatepicker: function (refName) {
var picker = new Pikaday({
field: this.$refs[refName],
format: 'YYYY-MM-DD'
})

this.$once('hook:beforeDestroy', function () {
picker.destroy()
})
}
}

循环引用

递归组件

组件是可以在它们自己的模板中调用自身的。不过它们只能通过 name 选项来做这件事:

1
name: "unique-name-of-my-component";

稍有不慎,递归组件就可能导致无限循环:

1
2
name: 'stack-overflow',
template: '<div><stack-overflow></stack-overflow></div>'

类似上述的组件将会导致“max stack size exceeded”错误,所以请确保递归调用是条件性的 (例如使用一个最终会得到 false 的 v-if)。

组件之间的循环引用

解决方法:

<tree-folder> 组件设为了那个点。我们知道那个产生悖论的子组件是 <tree-folder-contents> 组件,所以我们会等到生命周期钩子 beforeCreate 时去注册它:

1
2
3
beforeCreate: function () {
this.$options.components.TreeFolderContents = require('./tree-folder-contents.vue').default
}

或者,在本地注册组件的时候,你可以使用 webpack 的异步 import:

1
2
3
components: {
TreeFolderContents: () => import("./tree-folder-contents.vue");
}

模板定义的替代品

内联模板(inline-template)

当子组件中出现inline-template时,这个组件将会使用其里面的内容作为模板,而不是将其作为被分发的内容。

1
2
3
4
5
6
<my-component inline-template>
<div>
<p>These are compiled as the component's own template.</p>
<p>Not parent's transclusion content.</p>
</div>
</my-component>

内联模板需要定义在 Vue 所属的 DOM 元素内。

不过,inline-template 会让模板的作用域变得更加难以理解。所以作为最佳实践,请在组件内优先选择 template 选项或 .vue 文件里的一个 <template> 元素来定义模板。

X-template

另一个定义模板的方式是在一个 <script> 元素中,并为其带上 text/x-template 的类型,然后通过一个 id 将模板引用过去。例如:

1
2
3
4
5
6
7
8
9
//html
<script type="text/x-template" id="hello-world-template">
<p>Hello hello hello</p>
</script>;

//js
Vue.component("hello-world", {
template: "#hello-world-template",
});

x-template 需要定义在 Vue 所属的 DOM 元素外。

这些可以用于模板特别大的 demo 或极小型的应用,但是其它情况下请避免使用,因为这会将模板和该组件的其它定义分离开。

控制更新

强制更新

如果你发现你自己需要在 Vue 中做一次强制更新,99.9% 的情况,是你在某个地方做错了事。

你可能还没有留意到数组或对象的变更检测注意事项,或者你可能依赖了一个未被 Vue 的响应式系统追踪的状态。

然而,如果你已经做到了上述的事项仍然发现在极少数的情况下需要手动强制更新,那么你可以通过 $forceUpdate 来做这件事。

通过 v-once 创建低开销的静态组件

渲染包含大量静态内容的组件,你可以在根元素上添加 v-once 特性以确保这些内容只计算一次然后缓存起来;

1
2
3
4
5
6
7
8
Vue.component("terms-of-service", {
template: `
<div v-once>
<h1>Terms of Service</h1>
... a lot of static content ...
</div>
`,
});

注意事项:

不要过度使用这个模式。当需要渲染大量静态内容时,极少数的情况下它会给你带来便利,除非你非常留意渲染变慢了,不然它完全是没有必要的——再加上它在后期会带来很多困惑。例如,设想另一个开发者并不熟悉 v-once 或漏看了它在模板中,他们可能会花很多个小时去找出模板为什么无法正确更新。


title: 电话面试题 date: 2019-11-16 16:29:01

tags: 面试题

#CSS

  1. 重绘和回流(那些操作会引起重回和回流,如何减少回流)
  2. Css 伪类有哪些?
  3. Position 的几个属性,各个区别?
  4. Css3 常用的属性
  5. 实现水平垂直居中的方法
  6. Visible:none 和 display:none,hidden 的区别
  7. Css 的预处理器,好处,优点作用.
  8. Css 实现三角形.
  9. Css 引入的方法与区别

JS

  1. 闭包的理解,作用,场景,缺点.
  2. 类如何创建和继承
  3. 深拷贝有哪些
  4. 微任务和宏任务
  5. setInterval 如何实现,用 setTimeout 实现.
  6. for 循环里面有 setTimeout,里面有 console.log(i).不属于 for 循环最后面再有个 console.log(i).17:40 处.
  7. 如果把 var 改为 let,结果如何
  8. [] == ![]结果如何?为什么?
  9. Promise 的理解
  10. Async/Await 与 Promise 的区别和联系.

Await 相对于 Promise 相当于 Promise 的什么? 11. 随机生成一个 100-200 之间的数

Vue

  1. 虚拟 DOM 的理解
  2. Vue 如何实现双向绑定,底层
  3. 如何监听数组
  4. 异步组件在哪用,优点
  5. 计算属性和侦听器的区别(watch 和 computed)
  6. Slot 插槽,用在哪里.

独立配置 webpack?有过修改吗?

Vue 面试题

  1. 向后台请求数据放在哪个生命周期,为什么?
  2. Vue 的特性是数据驱动和组件化开发,有封装过自己的组件吗?
  3. 组件和 v-model,如果组件内数据变动,页面会不会都变动?
  4. V-model 的实现原理
  5. 路由菜单动态改变
  6. 请求菜单时,是每个请求都加上 token 还是加上其他东西?
  7. v-model 绑定相同类目在不同页面,点击其中一个,另一个会变化吗?为什么?

腾讯面试

1.ES6 导入为什么是 require?如何 require 模块?

2.密码加密用什么插件?这个插件是对称加密还是什么?

3.说下 http 状态码

4.缓存有几个阶段?浏览器请求网页,发现没有缓存到,就去重新请求,请问这个过程是怎样的?

5.如果遇到 304,会用到缓存的哪些字段?

6.前端工程化是什么?

7.webpack 插件和 loader 的区别

8.webpack 如何优化

8.vue 的底层原理

9.说下虚拟 dom

  1. js 的异步机制,底层原理是什么?

11.node.js 和 js 区别

12.计算机网络

#酒铺

  1. API 输出格式规范
  2. 单页和多页的区别
  3. 鉴权方式


title: 排序算法 date: 2019-12-05 15:26:08

tags:

算法和数据结构是相互依存的.

前端的数据结构主要用数组和对象.js 没有指针,无法使用链表.

排序算法

冒泡排序(体育委员两两对比)

时间复杂度:O(n)

需求: 数组 a=[3,2,4,5,1,6]从小到大排列打印出来.

方法: 找出最大的放在最右边,然后排前 5 个.然后依次把最大的放在最右边.

选择排序(体育老师点人)

时间复杂度:O(n)

方法: 选出最小的一个放在最前面.然后看剩下的.

剩下的最小的再放前面.

插入排序(排扑克牌)

时间复杂度:O(n)

声明空数组.取第一张牌,第二个数字和数组中比较,比最后一个大,就放后面,比最后一个小,就往前放.

基数排序(桶排序)(整理扑克牌)

优点:

和计数排序不同的是一个桶里可放多个数字,效率高,计数排序的改良版,时间复杂度:O(N+C)

缺点:

需要 hash 工具,和计数排序不同的是每个桶里都是无序的,还要再排一次序

用途:

高考总分排序,每 100 分放在一个桶里,数字很分散的时候不好用,可用基数排序(比如从几十到几千)

有重复的数.类似于 4 张 1,4 张 2,4 张 3.拿到 1 放到最前面,2 依次,如果还是 1,放在 1 那一摞.每一种放一摞.最后排序.

快排

优点:

效率高,时间复杂度:O(n*log2n)

缺点:

有时候比计数排序慢

挑选一个数,放中间,比它小的放左边,比它大的放右边.它的位置就固定了.

看前面的部分,再挑一个数,比它小的放左边,比它大的放右边.后面的部分,再挑一个数,比它小的放左边,比它大的放右边.又有两个数固定了.


title: 全栈开发后台资金管理系统项目date: 2019-11-27 15:58:28

tags: 项目

后端部分

1. nodemon 使用

当我们做服务器的时候,每次变更都要重启才能生效。

如:我们创建了一个名为 server.js 的文件,作为服务器

使用node ./server.js即可启动,但我们对其修改后,要看效果就要关闭之前的再启动。

而 nodemon 帮我们解决这个问题。

1
npm install nodemon -g //全局安装nodemon

然后就可以使用 nodemon 运行我们的服务器了

1
nodemon ./server.js

这时,修改文件,服务器会自动重启。

将命令设置到 package.json。

1
2
3
4
5
//在package.json中修改
"scripts": {
"start": "node server.js",
"server": "nodemon server.js"
},

这样在就可以使用npm run startnpm run server来运行服务器

2.连接数据库

1
2
//node-app下执行
npm install mongoose

为方便修改配置,新建文件 /config/keys.js, 内容:

1
2
3
4
module.exports = {
mongoURI:
"mongodb://<username>:<password>@cluster0-shard-00-00-oqdfe.mongodb.net:27017,cluster0-shard-00-01-oqdfe.mongodb.net:27017,cluster0-shard-00-02-oqdfe.mongodb.net:27017/test?ssl=true&replicaSet=Cluster0-shard-0&authSource=admin&retryWrites=true&w=majority"
};

该链接需要到 mongoDB 官网注册账户,获取 500M 免费空间,创建一个 Preject 再创建 Clusters,之后点击”Connect”,选择”Connect your Application”进入下一步,域名选择默认确定即可.DRIVER 选择“node.js”, VERSION 选择”2.2.12 or later”,然后 copy 下面的链接即可,注意修改:为对应的用户名和密码。可以在”Database Access”中添加和修改用户。

在 server.js 中引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//DB config
const db = require("./config/keys").mongoURI;

//连接数据库
//Connect to mongodb
mongoose.connect(db, {
//第一个参数db是在线数据库的地址,也可以直接将地址写入这里,美观起见,另写一个文件存储
useNewUrlParser: true,//防止弃用警告
useUnifiedTopology: true,//防止弃用警告
useFindAndModify: false //防止弃用警告
})
//提供promise调用
.then(() => console.log("mongoDB Connected")) //成功
.catch(err => console.log(err)); //失败

3.配置路由和接口

在 node-app 下创建 /route/api/users.js,内容:

用于登录和注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//users.js

//引入express
const express = require("express");
//实例化路由
const router = express.Router();

// $route GET api/users/test
//@desc 返回请求的json数据
//@access public(公有接口)

//验证路由,访问/test,将返回`msg:"login works"`
router.get("/test", (req, res) => {
//返回json数据
res.json({ msg: "login works" });
});

//导出router
module.exports = router;

在 server.js 中引用和使用

1
2
3
4
5
6
7
8
//server.js

const express = require('express')
const app = express()
//引入users.js
const users = require("./route/api/users");
//路由访问这个地址时,就会访问users
app.use("/api/users", users);

这时 使用浏览器访问 http://localhost:5000/api/users/test即可看到返回的msg:"login works"

4.创建模型

新建 /models/User.js,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//User.js

//引入mongoose.可以将数据存储到mongoose
const mongoose = require("mongoose");
//创建Schema模型
const Schema = mongoose.Schema;

//create Schema
const UserSchema = new Schema({
name: {
type: String,
required: true
},
email: {
type: String,
required: true
},
password: {
type: String
},
avatar: {
type: String,
required: true
},
date: {
type: Date,
default: Date.now
}
});

module.exports = User = mongoose.model("users", UserSchema);

4.5 下载postman,安装

可以用来测试接口是否通.

4.6 创建register接口

首先需要安装body-parser.

5.配置注册

安装 body-parser,方便发送 POST 请求

1
npm install body-parser

在 server.js 中引用

1
2
3
4
5
const bodyParser = require("body-parser");

//使用body-parser中间件
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());

在 users.js 中配置接口

1
2
3
4
5
6
//$route POST api/uers/register
//@desc 返回请求的JSON数据
//@access public (公有接口)
router.post("/register", (req, res) => {
console.log(req.body); //用来测试是否连接
});

//此处如果连接不上mongoDB,可能是白名单失效.再添加一个白名单在mongoDB即可.

功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//1.是否有邮箱

//
router.post("/register", (req, res) => {
//查询数据库中是否拥有邮箱
User.findOne({ email: req.body.email }).then(user => {
if (user) {
//如果存在
return res.status(400).json({ email: "邮箱已被注册!" });
} else {
//否则不存在
const newUser = new User({
name: req.body.name,
email: req.body.email,
password: req.body.password
});
}
});
});
密码加密

安装 bcrypt

1
npm install bcrypt

在 users.js 中引入,

1
const bcrypt = require("bcrypt");

官方详细说明链接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
bcrypt.genSalt(10, function(err, salt) {
//10是加密的一种模式
bcrypt.hash(newUser.password, salt, (err, hash) => {
//newUser.password 是加密对象,salt是回调函数,最后是加密结果
if (err) throw err; //如果存在错误,则抛出。 throw是js语法,抛出

//没有错误,则把加密过的密码hash赋值给password
newUser.password = hash;
//将newUser存储
newUser
.save()
.then(user => res.json(user))
.catch(err => console.log(err));
});
});

头像 avatar

gravatar 官方说明链接

安装

1
npm i gravatar

user.js 中引入

1
const gravatar = require("gravatar");

在接口位置使用(user.js)

1
2
3
4
5
6
7
8
9
10
11
if (user) {
return res.status(400).json({ email: "邮箱已被注册!" });
} else {
const avatar = gravatar.url(req.body.email, { s: "200", r: "pg", d: "mm" }); //s是大小。r是头像格式。mm是灰色的头像
const newUser = new User({
name: req.body.name,
email: req.body.email,
avatar, //引入头像
password: req.body.password
});
}

如何得到头像?

  1. 打开 gravatar 网址
  2. 注册 gravatar,其注册实际是注册了 wordpress.com 网站的账户,然后登录 gravatar,任意格式的邮箱均可申请成功,但无法收到邮件,则无法验证并修改头像。因此要使用可以收到验证的邮箱。
  3. 上传头像。上传图片时,最后会选择图片会有Choose a rating for your Gravatar ,有四个选项,G、PG、R、X,这里我们选择 pg,我们在使用时也是r: 'pg',需要保持一致。

这时,我们使用 postman 向 http://localhost:5000/api/users/register 发送 post 请求,使用(application/x-www-form-urlencoded)(key:email value:user@usertest.com) 就能得到设置的头像了。

7.登录接口

users.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//$route POST api/users/login
//@desc 返回taken jwt passport
//@access public (公有接口)
router.post("/login", (req, res) => {
const email = req.body.email;
const password = req.body.password;

//查询数据库,看email是否存在
User.findOne({ email }).then(user => {
if (!user) {
return res.status(404).json({ email: "用户不存在" }); //如果用户不存在
}
//如果email存在
//密码匹配
//第一个password是前端传入密码,user.password是系统内密码
bcrypt.compare(password, user.password).then(isMatch => {
if (isMatch) {
res.json({ msg: "success" }); //如果密码对比正确,(实际这里返回token,但暂时先返回msg)
} else {
return res.status(400).json({ password: "密码错误!" }); //如果密码对比不正确
}
});
});
});

返回 token

安装 jsonwebtoken (jwt)

1
npm install jsonwebtoken

在 users.js 引入

1
const jwt = require("jsonwebtoken");

在密码验证成功处插入

我们在 config/keys.js 导出的对象中,加入了 secretOrKey:”secret” 属性和值,再引入到 users.js 以方便统一管理配置。

过期时间的 3600 单位为秒

token 前必须是 “Bearer ”(送信人的意思),末尾空格也不可缺少。

如果 success 为 true,就应该得到 token 值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//替换上面的res.json({ msg: "success" });

//jwt.sign("规则","加密名字","过期时间","箭头函数")

const rule = { id: user.id, name: user.name }; //可以更多
//sign签名
jwt.sign(rule, keys.secretOrKey, { expiresIn: 3600 }, (err, token) => {
if (err) throw err;
res.json({
success: true,
//"Bearer "前缀是固定的,意思是送信者.后面有个空格
token: "Bearer " + token
});
});
// res.json({msg:"success"});

验证 token

token相当于一个令牌或者钥匙.

使用passport-jwt进行token验证.

users.js 加入接口

1
2
3
4
5
6
7
8
9
//$route GET api/users/current
//@desc return current user
//@access Privates

//router.get("/current", "验证token",(req, res) => {
//在中间验证token,但是需要passport,还没装,会报错,暂时删掉
router.get("/current", (req, res) => {
res.json({ msg: "success" }); //测试使用,后期有修改
});
安装 passport-jwt 和 passport
1
npm install passport-jwt passport

passport 网址

passport-jwt 网址

server.js 中引入,并初始化

1
2
3
4
5
//server.js

const passport = require("passport");
//passport初始化
app.use(passport.initialize());

新建文件 /config/passport.js,内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const JwtStrategy = require("passport-jwt").Strategy,
ExtractJwt = require("passport-jwt").ExtractJwt;
const mongoose = require("mongoose");
const User = mongoose.model("users");
const keys = require("../config/keys");

const opts = {};
opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken();
opts.secretOrKey = keys.secretOrKey;

module.exports = passport => {
passport.use(
new JwtStrategy(opts, (jwt_payload, done) => {
//console.log(jwt_payload);
//通过id获取用户
User.findById(jwt_payload.id)
//获取成功
.then(user => {
//如果用户存在
if (user) {
return done(null, user);
}
//如果用户不存在
return done(null, false);
})
//获取失败
.catch(err => console.log(err));
})
);
};

在 server.js 中引入 passport.js

1
2
3
4
//server.js

app.use(passport.initialize());
require("./config/passport")(passport); //这样代码就不需要在当前server.js中写了

这里使用了一个技巧,require("xxx.js")(对象) 将对象传入xxx.js,同时将该js引入当前文件中。这样就可以在xxx.js中编写代码,实现分离,而且在xxx.js可以使用传入的对象。

在 users.js 中引入 passport,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//users.js

const passport = require("passport");

//完成token验证,返回部分信息

//$route GET api/users/current
//@desc return current user
//@access Privates
router.get(
"/current",
passport.authenticate("jwt", { session: false }),
(req, res) => {
res.json({
id: req.user.id,
name: req.user.name,
email: req.user.email
});
}
);

这里调整了一些输出的内容,将输出对象改为了字符串,可能造成代码实际和上面有些出入。


添加身份

如果想在 user 中添加其他信息(比如添加管理员)可参考此内容

models/User.js 的 UserSchema 中添加身份字段

1
2
3
4
identity:{
type:String,
required:true
},

api/users.js 中加入信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// newUser中
const newUser = new User({
name: req.body.name,
email: req.body.email,
avatar,
password: req.body.password,
identity: req.body.identity, //添加的信息
});

//密码匹配规则中
const rule = {
id: user.id,
name: user.name,
avatar: user.avatar,
identity: user.identity,
};

//验证token输出信息时
router.get(
"/current",
passport.authenticate("jwt", { session: false }),
(req, res) => {
res.json({
id: req.user.id,
name: req.user.name,
email: req.user.email,
identity: req.user.identity, //添加的内容
});
}
);

配置信息接口

新建 models/Profile.js 建立 ProfileSchema,内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const mongoose = require("mongoose");
const Schema = mongoose.Schema;

//create Schema
const ProfileSchema = new Schema({
type: {
type: String,
},
describe: {
type: String,
},
income: {
type: String,
required: true,
},
expend: {
type: String,
required: true,
},
cash: {
type: String,
required: true,
},
remark: {
type: String,
},
date: {
type: Date,
default: Date.now,
},
});

module.exports = Profile = mongoose.model("profile", ProfileSchema);

新建 api/profiles.js 暂不写内容,将其在 server.js 中引入

1
2
3
4
5
//server.js
const profiles = require("./route/api/profiles");

//使用route
app.use("/api/profiles", profiles);

在 api/profiles.js 配置信息进行测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//@login & register
const express = require("express");
const router = express.Router();
const passport = require("passport");
const Profile = require("../../models/Profile");

//$route GET api/profile/test
//@desc 返回请求的JSON数据
//@access public (公有接口)
router.get("/test", (req, res) => {
res.json({ msg: "Profile works" });
});

module.exports = router;

postman 发送到 http://localhost:5000/api/profiles/test 返回 Profile works 即链接成功。

更改数据库接口

如果要更改数据库接口,可以/config/keys.js中的mongoURI的值,该值的获取方法,参考上述创建时的内容。

创建添加信息的接口

profiles.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//$route POST api/profile/add
//@desc 创建信息接口
//@access Private
router.post(
"/add",
passport.authenticate("jwt", { session: false }),
(req, res) => {
const profileFields = {};
if (req.body.type) profileFields.type = req.body.type;
if (req.body.describe) profileFields.describe = req.body.describe;
if (req.body.income) profileFields.income = req.body.income;
if (req.body.expend) profileFields.expend = req.body.expend;
if (req.body.cash) profileFields.cash = req.body.cash;
if (req.body.remark) profileFields.remark = req.body.remark;
new Profile(profileFields).save().then((profile) => {
res.json(profile);
});
}
);

获取所有信息

profiles.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//$route GET api/profile
//@desc 获取所有信息
//@access Private

router.get(
"/",
passport.authenticate("jwt", { session: false }),
(req, res) => {
Profile.find()
.then((profile) => {
if (!profile) {
return res.status(404).json("没有任何内容");
}
res.json(profile);
})
.catch((err) => res.status(404).json("err"));
}
);

获取单个信息

profiles.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//$route GET api/profile/:id
//@desc 获取单个信息
//@access Private

router.get(
"/:id",
passport.authenticate("jwt", {
session: false,
}),
(req, res) => {
Profile.findOne({ _id: req.params.id })
.then((profile) => {
if (!profile) {
return res.status(404).json("没有任何内容");
}
res.json(profile);
})
.catch((err) => res.status(404).json(err));
}
);

编辑信息

profiles.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//$route POST api/profile/edit
//@desc 编辑信息接口
//@access Private
router.post(
"/edit/:id",
passport.authenticate("jwt", {
session: false,
}),
(req, res) => {
const profileFields = {};
if (req.body.type) profileFields.type = req.body.type;
if (req.body.describe) profileFields.describe = req.body.describe;
if (req.body.income) profileFields.income = req.body.income;
if (req.body.expend) profileFields.expend = req.body.expend;
if (req.body.cash) profileFields.cash = req.body.cash;
if (req.body.remark) profileFields.remark = req.body.remark;
Profile.findOneAndUpdate(
{ _id: req.params.id },
{ $set: profileFields },
{ new: true }
).then((profile) => res.json(profile));
}
);

删除信息

profiles.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//$route delete api/profile/delete/:id
//@desc 删除信息接口
//@access Private
router.delete(
"/delete/:id",
passport.authenticate("jwt", {
session: false,
}),
(req, res) => {
Profile.findOneAndRemove({ _id: req.params.id })
.then((profile) => {
profile.save().then((profile) => res.json(profile));
})
.catch((err) => res.status(404).json("删除失败"));
}
);

至此,信息的增删改查均已实现。要创建其他 schema 可以参考此方式

前后端连载

查看 vue 版本,是否在 3.0.0 以上,我们要求是在 3.0.0 以上。

vue-cli 的安装见 vue 官网 ,这里就不说了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//终端中执行
vue -V //查看vue-cli版本 本案例要求3.0.0以上

//创建项目 client是自己起的名字,意为"客户端"
vue create client

接下来进入选择流程,后面 √ 为我们作出的选择项,-----表示回车到下一选项页
? Please pick a preset:
default (babel, eslint) (默认配置)
Manually select features (手动选择) √
-----
按键盘a表示全选,i表示反选,空格键 表示切换选中,如果你需要什么就选什么就可以了,这里选择BabelRouterVuex
? Please pick a preset: Manually select features
? Check the features needed for your project: (Press <space> to select, <a> to toggle all, <i> to invert selection)
❯◯ Babel
TypeScript
Progressive Web App (PWA) Support
Router
Vuex
CSS Pre-processors
Linter / Formatter
Unit Testing
E2E Testing
-----
是否使用history ,我们输入y,回车,会继续显示其他问题。
? Please pick a preset: Manually select features
? Check the features needed for your project: Babel, Router, Vuex
? Use history mode for router? (Requires proper server setup for index fallback in production) (Y/n)

In dedicated config files
In package.json

Save this as a preset for future projects? (y/N) (是否要保存你当前预制模板) N (第一次时可以保存一次方便之后用)
-----
接下来就是等待安装成功。 会产生一个 client的文件夹

//启动项目
cd client
npm run serve //注意,是serve 不是server

此时使用 http://localhost:8080/ 就可以打开前端了,再新建终端,执行 nodemon就打开了后台。

这需要两个终端打开,较为繁琐,因此采用前后端连载,借助 concurrently 将多个终端启动的项目绑在一起

安装 concurrently

1
npm install concurrently

打开 /client/package.json

1
2
3
4
5
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"start": "npm run serve"
},

此时,我们在 client 中 使用 npm run start即可启动前端

在根目录的 package.json 中配置client-stallclientdev

1
2
3
4
5
6
7
"scripts": {
"client-install":"npm install --prefix client",
"client":"npm start --prefix client",
"start": "node server.js",
"server": "nodemon server.js",
"dev":"concurrently \"npm run server\" \"npm run client\""
},

此时我们可以在根目录的终端下执行npm run dev即可同时启动 前端和后台

前端部分

接后端部分,此文档为 前端部分 内容。

准备工作

为使内容整洁,我们将 vue-cli 创建项目时生成的我们不需要的文件进行整理

我们接下来更多的是在 client 这个文件夹下工作,非强调指明,则以 client 视为根目录。

  • 删除 /src/assets/logo.png  (vue 的 logo 图片)
  • 删除 /src/components/HelloWorld.vue
  • 删除 /src/views/ 中的 About.vue 和 Home.vue
  • 新建 /src/views/Index.vue,内容为
1
2
3
4
5
6
7
8
9
10
<template>
<div class="index">初始化页面</div>
</template>

<script>
export default {
name: "index",
components: {},
};
</script>
  • 打开 /src/router.js,重新整理为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import Vue from "vue";
import Router from "vue-router";
import Index from "./views/Index.vue";

Vue.use(Router);

export default new Router({
mode: "history",
base: process.env.BASE_URL,
routes: [
{
path: "/",
redirect: "/index",
},
{
path: "/index",
name: "index",
component: Index,
},
],
});
  • 打开 /src/App.vue,重新整理为
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<div id="app">
<router-view />
</div>
</template>

<style>
html,
body,
#app {
width: 100%;
height: 100%;
}
</style>
  • 新建 /public/css/reset.css ,在 /public/index.html 中引入该 css 文件
1
<link rel="stylesheet" href="css/reset.css" />

reset.css 内容可以访问 CSS reset得到,也可在下面设置自己需要的初始样式

本案例中,我们在 reset.css 中追加了 el 中加载相关的样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.el-loading {
position: absolute;
z-index: 2000;
background-color: rgba(255, 255, 255, 0.7);
margin: 0;
padding: 0;
top: 0;
right: 0;
bottom: 0;
left: 0;
-webkit-transition: opacity 0.3s;
transition: opacity 0.3s;
}
.el-loading-spinner {
top: 50%;
margin-top: -21px;
width: 100%;
text-align: center;
position: absolute;
}

注册页和 404

安装 elementUI

1
2
//此时目录在client中
npm i element-ui -S

在/src/main.js 中引入

1
2
3
4
import ElementUI from "element-ui";
import "element-ui/lib/theme-chalk/index.css";

Vue.use(ElementUI);

将 图片文件 放在 /src/asset/ 文件夹下面

分别是 404.gif,bg.jpg,logo.png,showcase.png

初始注册,新建 /src/views/Register.vue,内容 (只是简单布局,还未设置表单内容)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<template>
<div class="register">
<section class="form_container">
<div class="manage_tip">
<span class="title">万事屋在线后台管理系统</span>

</div>
</section>
</div>
</template>

<script>
export default {
name:"register",
components:{}
};
</script>

<style scoped>
.register{
position: relative;
width: 100%;
height: 100%;
background: url(../assets/bg.jpg) no-repeat center center;
background-size: 100% 100%;
}
.form_container{
width: 370px;
height: 210px;
position: absolute;
top:10%;
left:34%;
padding:25px;
border-radius: 5px;
text-align: center;
}
.form_container .manage_tip .title{
font-family: 'Microsooft YaHei';
font-weight: bold;
font-size: 26px;
color: #fff;
}
</style>

在 router.js 中设置路由

1
2
3
4
5
6
7
8
9
//引入组件
import Register from './views/Register.vue'

//添加路由
{
path: '/register',
name:'register',
component: Register
},

设置 404 页面

  1. 新建 /src/views/404.vue 组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<div class="notfound">
<img src="../assets/404.png" alt="页面没找到">
</div>
</template>

<style scoped>
.notfound{
width: 100%;
height:100%;
overflow: hidden;
}
.notfound img{
width: 100%;
height:100%;
}
</style>

在 router.js 中设置路由

1
2
3
4
5
6
//router.js
{
path: '*',
name:'/404',
component: NotFound
}

注册表单

此后大量使用 element 的代码,为避免过长,仅提一些重要的点,其他请结合原文件阅读笔记

密码规则与验证

加载动画和消息提示

安装 axios

1
2
//client目录下
npm install axios

新建 /src/http.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import axios from "axios";
import { Message, Loading } from "element-ui";

let loading;
function startLoading() {
loading = Loading.service({
lock: true,
text: "拼命加载中...",
background: "rgba(0,0,0,0.7)",
});
}

function endLoading() {
loading.close();
}

//请求拦截
axios.interceptors.request.use(
(config) => {
//加载动画
startLoading();
return config;
},
(error) => {
return Promise.reject(error);
}
);

//响应拦截

axios.interceptors.response.use(
(response) => {
//结束加载动画
endLoading();
return response;
},
(error) => {
//错误提醒
endLoading();
Message.error(error.response.data);
return Promise.reject(error);
}
);

export default axios;

在 main.js 中引用

1
2
3
import axios from "./http";

Vue.prototype.$axios = axios;

http.js 内容(后会变动)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import axios from "axios";
import { Message, Loading } from "element-ui";

let loading;
function startLoading() {
loading = Loading.service({
lock: true,
text: "拼命加载中...",
background: "rgba(0,0,0,0.7)",
});
}

function endLoading() {
loading.close();
}

//请求拦截
axios.interceptors.request.use(
(config) => {
//加载动画
startLoading();
return config;
},
(error) => {
return Promise.reject(error);
}
);

//响应拦截

axios.interceptors.response.use(
(response) => {
//结束加载动画
endLoading();
return response;
},
(error) => {
//错误提醒
endLoading();
Message.error(error.response.data);
return Promise.reject(error);
}
);

export default axios;

配置前端跨域请求(使用 vue-cli 项目)

新建 vue.config.js ,在 client 目录下,内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
const path = require("path");
const debug = process.env.NODE_ENV !== "production";

module.exports = {
baseUrl: "/", //根域上下文目录
outputDir: "dist", //构建输出目录
assetsDir: "assets", //静态资源目录(js,css,img,fonts)
lintOnSave: false, //是否开启eslint保存检测,有效值:true|false|'error'
runtimeCompiler: true, //运行时版本是否需要编译
transpileDependencies: [], //默认babel-loader忽略node_modules,这里可增加例外的依赖包名
productionSourceMap: true, //是否在构建生产包时生产 sourceMap 文件,false将提高构建速度
configureWebpack: (config) => {
//webpack 配置,值为对象时会合并配置,为方法时会改写配置
if (debug) {
//开发环境配置
config.devtool = "cheap-module-eval-source-map";
} else {
//生产环境配置
}
// Object.assign(config,{ //开发生产共同配置
// resolve:{
// alias:{
// '@':path.resolve(__dirname,'./src'),
// '@c':path.resolve(__dirname,'./src/components'),
// 'vue$':'vue/dist/vue.esm.js'
// }
// }
// })
},
chainWebpack: (config) => {
//webpack链接API,用于生成和修改webpack配置,
// https://github.com/vuejs/vue-cli/blob/dev/docs/webpack.md
if (debug) {
//本地开发配置
} else {
//生产开发配置
}
},
parallel: require("os").cpus().length > 1, //构建时开启多进程处理babel编译
pluginOptions: {
//第三方插件配置
},
pwa: {
//单页插件相关配置
// https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-pwa
},
devServer: {
open: true,
host: "localhost",
port: 8080,
https: false,
hotOnly: false,
proxy: {
//配置跨域
"/api": {
target: "http://localhost:5000/api",
ws: true,
changOrigin: true,
pathRewrite: {
"^/api": "",
},
},
},
before: (app) => {},
},
};

这样就可以访问我们的后台了

在 register.vue 中配置跳转,这样就可以注册用户了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
submitForm(formName) {
this.$refs[formName].validate(valid => {
if (valid) {
this.$axios
.post("/api/users/register", this.registerUser)
.then(res => {
//注册成功
this.$message({
message: "账号注册成功!",
type: "success"
});
});
this.$router.push("/login");
}
});
}

登录逻辑

新建组件和添加路由参考注册,这里讲下登录逻辑. Login.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
submitForm(formName) {
this.$refs[formName].validate(valid => {
if (valid) {
this.$axios.post("/api/users/login", this.loginUser).then(res => {
//拿到token
const { token } = res.data;
// 存储到localStorage
localStorage.setItem("eleToken", token);
this.$router.push('/index');
});
this.$router.push("/login");
}
});
}

路由守卫

router.js

1
2
3
4
5
6
7
8
9
10
11
//路由守卫
router.beforeEach((to, from, next) => {
//如果token存在返回boolean值true,否则false
const isLogin = localStorage.eleToken ? true : false;
if (to.path == "/login" || to.path == "/register") {
next();
} else {
//如果有token,为true,就正常跳转;为false,就跳转到登录页
isLogin ? next() : next("login");
}
});

设置 token 和 token 过期

在请求拦截中,如果存在 token,就把 token 设置到请求头中.

在响应拦截里的 error 里,如果状态码是 401 未授权,表示 token 过期.就在 error 返回函数里清除 token,并跳转到登录页.

http.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//请求拦截
axios.interceptors.request.use(
(config) => {
//加载动画
startLoading();
//如果有token
if (localStorage.eleToken) {
//设置统一的请求头header
config.headers.Authorization = localStorage.eleToken;
}

return config;
},
(error) => {
return Promise.reject(error);
}
);

//响应拦截
axios.interceptors.response.use(
(response) => {
//结束加载动画
endLoading();
return response;
},
(error) => {
//错误提醒
endLoading();
Message.error(error.response.data);

//获取错误状态码
const { status } = error.response;
//401未授权,表示token过期
if (status == 401) {
Message.error("token失效,请重新登录!");
//清除token
localStorage.removeItem("eleToken");
//跳转到登录页面
router.push("/login");
}
return Promise.reject(error);
}
);

解析 token 存储到 Vuex 中

安装解析 token 的模块

1
npm install jwt-decode
1
2
3
4
5
6
7
8
9
10
11
//Login.vue

//引入解析模块
import jwt_decode from "jwt_decode";

//解析token
const decoded = jwt_decode(token);

//token存储到vuex中
this.$store.dispatch("setAuthenticated", !this.isEmpty(decoded));
this.$store.dispatch("setUser", decoded);

设置 Vuex

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
//store.js

//登录成功后将数据存储到Vuex中
//判断是否认证通过
const types = {
SET_AUTHENTICATED: "SET_AUTHENTICATED",
SET_USER: "SET_USER",
};

const state = {
isAuthenticated: false,
user: {},
};

const getters = {
isAuthenticated: (state) => state.isAuthenticated,
user: (state) => state.user,
};

const mutations = {
[types.SET_AUTHENTICATED](state, isAuthenticated) {
if (isAuthenticated) state.isAuthenticated = isAuthenticated;
else state.isAuthenticated = false;
},

[types.SET_USER](state, user) {
if (user) state.user = user;
else state.user = {};
},
};

const actions = {
setAuthenticated: ({ commit }, isAuthenticated) => {
commit(types.SET_AUTHENTICATED, isAuthenticated);
},
setUser: ({ commit }, user) => {
commit(types.SET_USER, user);
},
//清除当前的状态
clearCurrentState: ({ commit }) => {
commit(types.SET_AUTHENTICATED, false);
commit(types.SET_USER, null);
},
};

export default new Vuex.Store({
state,
getters,
mutations,
actions,
});

方法: 判断是否为空

1
2
3
4
5
6
7
isEmpty(value){
return (
value === undefined ||
value === null ||
(typeof value === "object" && Object.keys(value).length === 0) ||
(typeof value === "string" && value.trim().length === 0)
)

在根组件 App.vue 中判断 token

1
2
3
4
5
6
7
8
9
created() {
if (localStorage.eleToken) {
//解析token
const decoded = jwt_decode(localStorage.eleToken);
//token存储到vuex中
this.$store.dispatch("setAuthenticated", !this.isEmpty(decoded));
this.$store.dispatch("setUser", decoded);
}
}

样式

新建/component/HeadNav.vue

将 HeadNav.vue 引入到 Index.vue,并注册,然后 template 中调用

在 HeadNav.vue 中布局

1
//看代码

写向下箭头的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
  methods: {
setDialogInfo(cmdItem) {
switch (cmdItem) {
case "info":
this.showInfoList();
break;
case "logout":
this.logout();
break;
}
},
showInfoList() {
console.log("个人信息");
},
logout() {
//清除token
localStorage.removeItem("eleToken");
//设置Vuex store
this.$store.dispatch("clearCurrentState");
//跳转到login
this.$router.push("/login");
}
}
};
//vuex中记得在actions中添加clearCurrentState

个人信息

新建 views/Home.vue

在 router.js 中设置二级路由

新建 views/InfoShow.vue

侧面导航栏

新建 assets/component/LeftMenu.vue

编辑收支类型

创建资金列表

新建 views/FundList.vue

添加各个按钮,事件

设置添加按钮,新增对话框 component/Dialog.vue 组件

编辑和添加

编辑和添加功能雷同,把 formData 放到父级 fundlist 中,并用 props 传递

修改父级中 dialog 的属性,新增 title,options,方便切换弹窗的标题和选项

在 handleEdit 中修改 dialog 的 title,当点击编辑时 title 就切换成’编辑’

编辑时已经拿到数据了.this.formData 的值也就是传入值了

同样,添加的也可以新增 this.formData.但是添加的数据默认是空的

在 onSubmit 中判断提交的类型

1
const url = this.dialog.option == "add" ? "add" : `edit/${this.formData.id}`;

删除按钮

1
2
this.$axios.delete(`/api/profiles/delete/${row._id}`);
//之后可以then调用$message弹出删除成功的提示

分页

elementUI 布局整行分为 24 列.

使用标准分页

1
2
3
4
5
6
7
8
9
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage4"
:page-sizes="[100, 200, 300, 400]"
:page-size="100"
layout="total, sizes, prev, pager, next, jumper"
:total="400">
</el-pagination>

//修改绑定数据

//设置全部数据容器(数组)

allTableData: []

//在获取数据时就开始设置分页数据

this.setPaginations()

在 setPaginations 中设置默认属性

筛选和权限

定义筛选组件

复制 elementUI 的时间选择器.

添加筛选按钮,绑定筛选事件 handleSearch()

绑定开始时间,结束时间.在 data 中定义开始时间 startTime,结束时间 endTime

添加过滤容器 filterTableData:{},在 getProfile()时也存储一次.

权限

使用计算属性 computed 获取此时用户的身份

1
2
3
4
computed: {
user(){
return this.$store.getters.user
}

使用 v-if 决定是否可以使用添加,编辑,删除操作.

1
2
v-if="user.indentity == 'manager'"
//将此判断加到添加事件之前和label='操作'后


title: 前端常见跨域解决方案 date: 2019-12-03 12:34:54

tags: HTML
categories: # 这里写的分类会自动汇集到 categories 页面上,分类可以多级 # 这里写的分类会自动汇集到 categories 页面上,分类可以多级

  • HTML # 一级分类
  • HTTP 基础 # 二级分类

什么是跨域?

跨域是指一个域下的文档或脚本试图去请求另一个域下的资源,这里跨域是广义的。

广义的跨域:

1.) 资源跳转: A 链接、重定向、表单提交

2.) 资源嵌入: <link>、<script>、<img>、<frame>等 dom 标签,还有样式中background:url()@font-face()等文件外链

3.) 脚本请求: js 发起的 ajax 请求、dom 和 js 对象的跨域操作等

其实我们通常所说的跨域是狭义的,是由浏览器同源策略限制的一类请求场景。

跨域建议方案

简单的跨域请求 jsonp 即可,复杂的 cors,窗口之间 JS 跨域 postMessage,开发环境下接口跨域用 nginx 反向代理或 node 中间件比较方便。

什么是同源策略?

同源策略/SOP(Same origin policy)是一种约定,由 Netscape 公司 1995 年引入浏览器,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到 XSS、CSFR 等攻击。所谓同源是指”协议+域名+端口”三者相同,即便两个不同的域名指向同一个 ip 地址,也非同源。

同源策略限制以下几种行为:

1.) Cookie、LocalStorage 和 IndexDB 无法读取

2.) DOM 和 Js 对象无法获得

3.) AJAX 请求不能发送

常见跨域场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
URL                                      说明                    是否允许通信
http://www.domain.com/a.js
http://www.domain.com/b.js 同一域名,不同文件或路径 允许
http://www.domain.com/lab/c.js

http://www.domain.com:8000/a.js
http://www.domain.com/b.js 同一域名,不同端口 不允许

http://www.domain.com/a.js
https://www.domain.com/b.js 同一域名,不同协议 不允许

http://www.domain.com/a.js
http://192.168.4.12/b.js 域名和域名对应相同ip 不允许

http://www.domain.com/a.js
http://x.domain.com/b.js 主域相同,子域不同 不允许
http://domain.com/c.js

http://www.domain1.com/a.js
http://www.domain2.com/b.js 不同域名 不允许

跨域解决方案

1、 通过 jsonp 跨域

2、 document.domain + iframe 跨域

3、 location.hash + iframe

4、 window.name + iframe 跨域

5、 postMessage跨域

6、 跨域资源共享(CORS)

7、 nginx 代理跨域

8、 nodejs 中间件代理跨域

9、 WebSocket 协议跨域

一、 通过 jsonp 跨域

通常为了减轻 web 服务器的负载,我们把 js、css,img 等静态资源分离到另一台独立域名的服务器上,在 html 页面中再通过相应的标签从不同域名下加载静态资源,而被浏览器允许,基于此原理,我们可以通过动态创建 script,再请求一个带参网址实现跨域通信。

1.)原生实现:

1
2
3
4
5
6
7
8
9
10
11
12
var script = document.createElement("script");
script.type = "text/javascript";

// 传参一个回调函数名给后端,方便后端返回时执行这个在前端定义的回调函数
script.src =
"http://www.domain2.com:8080/login?user=admin&callback=handleCallback";
document.head.appendChild(script);

// 回调执行函数
function handleCallback(res) {
alert(JSON.stringify(res));
}

服务端返回如下(返回时即执行全局函数):

1
handleCallback({ status: true, user: "admin" });

2.)jquery ajax:

1
2
3
4
5
6
7
$.ajax({
url: "http://www.domain2.com:8080/login",
type: "get",
dataType: "jsonp", // 请求方式为jsonp
jsonpCallback: "handleCallback", // 自定义回调函数名
data: {},
});

3.)vue.js:

1
2
3
4
5
6
7
8
this.$http
.jsonp("http://www.domain2.com:8080/login", {
params: {},
jsonp: "handleCallback",
})
.then((res) => {
console.log(res);
});

后端 node.js 代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var querystring = require("querystring");
var http = require("http");
var server = http.createServer();

server.on("request", function (req, res) {
var params = qs.parse(req.url.split("?")[1]);
var fn = params.callback;

// jsonp返回设置
res.writeHead(200, { "Content-Type": "text/javascript" });
res.write(fn + "(" + JSON.stringify(params) + ")");

res.end();
});

server.listen("8080");
console.log("Server is running at port 8080...");

jsonp 缺点:只能实现 get 一种请求。

二、 document.domain + iframe 跨域

此方案仅限主域相同,子域不同的跨域应用场景。

实现原理:两个页面都通过 js 强制设置 document.domain 为基础主域,就实现了同域。

1.)父窗口:(http://www.domain.com/a.html)

1
2
3
4
5
<iframe id="iframe" src="http://child.domain.com/b.html"></iframe>
<script>
document.domain = 'domain.com';
var user = 'admin';
</script>

2.)子窗口:(http://child.domain.com/b.html)

1
2
3
4
<script>
document.domain = 'domain.com'; // 获取父窗口中变量 alert('get js data from
parent ---> ' + window.parent.user);
</script>

三、 location.hash + iframe 跨域

实现原理: a 欲与 b 跨域相互通信,通过中间页 c 来实现。 三个页面,不同域之间利用 iframe 的location.hash传值,相同域之间直接 js 访问来通信。

具体实现:A 域:a.html -> B 域:b.html -> A 域:c.html,a 与 b 不同域只能通过 hash 值单向通信,b 与 c 也不同域也只能单向通信,但 c 与 a 同域,所以 c 可通过 parent.parent 访问 a 页面所有对象。

1.)a.html:(http://www.domain1.com/a.html)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
<script>
var iframe = document.getElementById('iframe');

// 向b.html传hash值
setTimeout(function() {
iframe.src = iframe.src + '#user=admin';
}, 1000);

// 开放给同域c.html的回调方法
function onCallback(res) {
alert('data from c.html ---> ' + res);
}
</script>

2.)b.html:(http://www.domain2.com/b.html)

1
2
3
4
5
6
7
8
9
<iframe id="iframe" src="http://www.domain1.com/c.html" style="display:none;"></iframe>
<script>
var iframe = document.getElementById('iframe');

// 监听a.html传来的hash值,再传给c.html
window.onhashchange = function () {
iframe.src = iframe.src + location.hash;
};
</script>

3.)c.html:(http://www.domain1.com/c.html)

1
2
3
4
5
6
7
<script>
// 监听b.html传来的hash值
window.onhashchange = function () {
// 再通过操作同域a.html的js回调,将结果传回
window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', ''));
};
</script>

四、 window.name + iframe 跨域

window.name属性的独特之处:name 值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)。

1.)a.html:(http://www.domain1.com/a.html)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
var proxy = function (url, callback) {
var state = 0;
var iframe = document.createElement("iframe");

// 加载跨域页面
iframe.src = url;

// onload事件会触发2次,第1次加载跨域页,并留存数据于window.name
iframe.onload = function () {
if (state === 1) {
// 第2次onload(同域proxy页)成功后,读取同域window.name中数据
callback(iframe.contentWindow.name);
destoryFrame();
} else if (state === 0) {
// 第1次onload(跨域页)成功后,切换到同域代理页面
iframe.contentWindow.location = "http://www.domain1.com/proxy.html";
state = 1;
}
};

document.body.appendChild(iframe);

// 获取数据以后销毁这个iframe,释放内存;这也保证了安全(不被其他域frame js访问)
function destoryFrame() {
iframe.contentWindow.document.write("");
iframe.contentWindow.close();
document.body.removeChild(iframe);
}
};

// 请求跨域b页面数据
proxy("http://www.domain2.com/b.html", function (data) {
alert(data);
});

2.)proxy.html:(http://www.domain1.com/proxy....)

中间代理页,与 a.html 同域,内容为空即可。

3.)b.html:(http://www.domain2.com/b.html)

1
<script>window.name = 'This is domain2 data!';</script>

总结:通过 iframe 的 src 属性由外域转向本地域,跨域数据即由 iframe 的window.name从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。

五、 postMessage 跨域

postMessage 是 HTML5 XMLHttpRequest Level 2 中的 API,且是为数不多可以跨域操作的 window 属性之一,它可用于解决以下方面的问题:

a.) 页面和其打开的新窗口的数据传递

b.) 多窗口之间消息传递

c.) 页面与嵌套的 iframe 消息传递

d.) 上面三个场景的跨域数据传递

用法:postMessage(data,origin)方法接受两个参数

data: html5 规范支持任意基本类型或可复制的对象,但部分浏览器只支持字符串,所以传参时最好用 JSON.stringify()序列化。

origin: 协议+主机+端口号,也可以设置为”*“,表示可以传递给任意窗口,如果要指定和当前窗口同源的话设置为”/“。

1.)a.html:(http://www.domain1.com/a.html)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
<script>
var iframe = document.getElementById('iframe');
iframe.onload = function() {
var data = {
name: 'aym'
};
// 向domain2传送跨域数据
iframe.contentWindow.postMessage(JSON.stringify(data), 'http://www.domain2.com');
};

// 接受domain2返回数据
window.addEventListener('message', function(e) {
alert('data from domain2 ---> ' + e.data);
}, false);
</script>

2.)b.html:(http://www.domain2.com/b.html)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script>
// 接收domain1的数据
window.addEventListener('message', function(e) {
alert('data from domain1 ---> ' + e.data);

var data = JSON.parse(e.data);
if (data) {
data.number = 16;

// 处理后再发回domain1
window.parent.postMessage(JSON.stringify(data), 'http://www.domain1.com');
}
}, false);
</script>

六、 跨域资源共享(CORS)

普通跨域请求:只服务端设置 Access-Control-Allow-Origin 即可,前端无须设置,若要带 cookie 请求:前后端都需要设置。

需注意的是:由于同源策略的限制,所读取的 cookie 为跨域请求接口所在域的 cookie,而非当前页。如果想实现当前页 cookie 的写入,可参考下文:七、nginx 反向代理中设置proxy_cookie_domain 和 八、NodeJs 中间件代理中 cookieDomainRewrite 参数的设置。

目前,所有浏览器都支持该功能(IE8+:IE8/9 需要使用 XDomainRequest 对象来支持 CORS)),CORS 也已经成为主流的跨域解决方案。

1、 前端设置:

1.)原生 ajax

1
2
// 前端设置是否带cookie
xhr.withCredentials = true;

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var xhr = new XMLHttpRequest(); // IE8/9需用window.XDomainRequest兼容

// 前端设置是否带cookie
xhr.withCredentials = true;

xhr.open("post", "http://www.domain2.com:8080/login", true);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.send("user=admin");

xhr.onreadystatechange = function () {
if (xhr.readyState == 4 && xhr.status == 200) {
alert(xhr.responseText);
}
};

2.)jQuery ajax

1
2
3
4
5
6
7
8
$.ajax({
...
xhrFields: {
withCredentials: true // 前端设置是否带cookie
},
crossDomain: true, // 会让请求头中包含跨域的额外信息,但不会含cookie
...
});

3.)vue 框架

a.) axios 设置:

1
axios.defaults.withCredentials = true;

b.) vue-resource 设置:

1
Vue.http.options.credentials = true;

2、 服务端设置:

若后端设置成功,前端浏览器控制台则不会出现跨域报错信息,反之,说明没设成功。

1.)Java 后台:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*
* 导入包:import javax.servlet.http.HttpServletResponse;
* 接口参数中定义:HttpServletResponse response
*/

// 允许跨域访问的域名:若有端口需写全(协议+域名+端口),若没有端口末尾不用加'/'
response.setHeader("Access-Control-Allow-Origin", "http://www.domain1.com");

// 允许前端带认证cookie:启用此项后,上面的域名不能为'*',必须指定具体的域名,否则浏览器会提示
response.setHeader("Access-Control-Allow-Credentials", "true");

// 提示OPTIONS预检时,后端需要设置的两个常用自定义头
response.setHeader(
"Access-Control-Allow-Headers",
"Content-Type,X-Requested-With"
);

2.)Nodejs 后台示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
var http = require("http");
var server = http.createServer();
var qs = require("querystring");

server.on("request", function (req, res) {
var postData = "";

// 数据块接收中
req.addListener("data", function (chunk) {
postData += chunk;
});

// 数据接收完毕
req.addListener("end", function () {
postData = qs.parse(postData);

// 跨域后台设置
res.writeHead(200, {
"Access-Control-Allow-Credentials": "true", // 后端允许发送Cookie
"Access-Control-Allow-Origin": "http://www.domain1.com", // 允许访问的域(协议+域名+端口)
/*
* 此处设置的cookie还是domain2的而非domain1,因为后端也不能跨域写cookie(nginx反向代理可以实现),
* 但只要domain2中写入一次cookie认证,后面的跨域接口都能从domain2中获取cookie,从而实现所有的接口都能跨域访问
*/
"Set-Cookie": "l=a123456;Path=/;Domain=www.domain2.com;HttpOnly", // HttpOnly的作用是让js无法读取cookie
});

res.write(JSON.stringify(postData));
res.end();
});
});

server.listen("8080");
console.log("Server is running at port 8080...");

七、 nginx 代理跨域

1、 nginx 配置解决 iconfont 跨域

浏览器跨域访问 js、css、img 等常规静态资源被同源策略许可,但 iconfont 字体文件(eot|otf|ttf|woff|svg)例外,此时可在 nginx 的静态资源服务器中加入以下配置。

1
2
3
location / {
add_header Access-Control-Allow-Origin *;
}

2、 nginx 反向代理接口跨域

跨域原理: 同源策略是浏览器的安全策略,不是 HTTP 协议的一部分。服务器端调用 HTTP 接口只是使用 HTTP 协议,不会执行 JS 脚本,不需要同源策略,也就不存在跨越问题。

实现思路:通过 nginx 配置一个代理服务器(域名与 domain1 相同,端口不同)做跳板机,反向代理访问 domain2 接口,并且可以顺便修改 cookie 中 domain 信息,方便当前域 cookie 写入,实现跨域登录。

nginx 具体配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#proxy服务器
server {
listen 81;
server_name www.domain1.com;

location / {
proxy_pass http://www.domain2.com:8080; #反向代理
proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名
index index.html index.htm;

# 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用
add_header Access-Control-Allow-Origin http://www.domain1.com; #当前端只跨域不带cookie时,可为*
add_header Access-Control-Allow-Credentials true;
}
}

1.) 前端代码示例:

1
2
3
4
5
6
7
8
var xhr = new XMLHttpRequest();

// 前端开关:浏览器是否读写cookie
xhr.withCredentials = true;

// 访问nginx中的代理服务器
xhr.open("get", "http://www.domain1.com:81/?user=admin", true);
xhr.send();

2.) Nodejs 后台示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var http = require("http");
var server = http.createServer();
var qs = require("querystring");

server.on("request", function (req, res) {
var params = qs.parse(req.url.substring(2));

// 向前台写cookie
res.writeHead(200, {
"Set-Cookie": "l=a123456;Path=/;Domain=www.domain2.com;HttpOnly", // HttpOnly:脚本无法读取
});

res.write(JSON.stringify(params));
res.end();
});

server.listen("8080");
console.log("Server is running at port 8080...");

八、 Nodejs 中间件代理跨域

node 中间件实现跨域代理,原理大致与 nginx 相同,都是通过启一个代理服务器,实现数据的转发,也可以通过设置 cookieDomainRewrite 参数修改响应头中 cookie 中域名,实现当前域的 cookie 写入,方便接口登录认证。

1、 非 vue 框架的跨域(2 次跨域)

利用 node + express + http-proxy-middleware 搭建一个 proxy 服务器。

1.)前端代码示例:

1
2
3
4
5
6
7
8
var xhr = new XMLHttpRequest();

// 前端开关:浏览器是否读写cookie
xhr.withCredentials = true;

// 访问http-proxy-middleware代理服务器
xhr.open("get", "http://www.domain1.com:3000/login?user=admin", true);
xhr.send();

2.)中间件服务器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var express = require("express");
var proxy = require("http-proxy-middleware");
var app = express();

app.use(
"/",
proxy({
// 代理跨域目标接口
target: "http://www.domain2.com:8080",
changeOrigin: true,

// 修改响应头信息,实现跨域并允许带cookie
onProxyRes: function (proxyRes, req, res) {
res.header("Access-Control-Allow-Origin", "http://www.domain1.com");
res.header("Access-Control-Allow-Credentials", "true");
},

// 修改响应信息中的cookie域名
cookieDomainRewrite: "www.domain1.com", // 可以为false,表示不修改
})
);

app.listen(3000);
console.log("Proxy server is listen at port 3000...");

3.)Nodejs 后台同(六:nginx)

2、 vue 框架 Vue-cli2.0 的跨域(1 次跨域)

利用 node + webpack + webpack-dev-server 代理接口跨域。在开发环境下,由于 vue 渲染服务和接口代理服务都是webpack-dev-server同一个,所以页面与代理接口之间不再跨域,无须设置 headers 跨域信息了。

webpack.config.js 部分配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//webpack.config.js
module.exports = {
entry: {},
module: {},
...
devServer: {
historyApiFallback: true,
proxy: [{
context: '/login',
target: 'http://www.domain2.com:8080', // 代理跨域目标接口
changeOrigin: true,
secure: false, // 当代理某些https服务报错时用
cookieDomainRewrite: 'www.domain1.com' // 可以为false,表示不修改
}],
noInfo: true
}
}

3. vue-cli3.0 的跨域

在根目录新建vue.config.js.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//vue.config.js
module.exports = {
devServer: {
open: true,
host: "localhost",
port: 8080,
https: false,
hotOnly: false,
proxy: {
//配置跨域
"/api": {
target: "http://localhost:5000/api",
ws: true, //websockets
changOrigin: true,
pathRewrite: {
"^/api": "",
},
},
},
},
};

九、 WebSocket 协议跨域

WebSocket protocol 是 HTML5 一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是 server push 技术的一种很好的实现。

原生 WebSocket API 使用起来不太方便,我们使用 Socket.io,它很好地封装了 webSocket 接口,提供了更简单、灵活的接口,也对不支持 webSocket 的浏览器提供了向下兼容。

1.)前端代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<div>user input:<input type="text"></div>
<script src="https://cdn.bootcss.com/socket.io/2.2.0/socket.io.js"></script>
<script>
var socket = io('http://www.domain2.com:8080');

// 连接成功处理
socket.on('connect', function() {
// 监听服务端消息
socket.on('message', function(msg) {
console.log('data from server: ---> ' + msg);
});

// 监听服务端关闭
socket.on('disconnect', function() {
console.log('Server socket has closed.');
});
});

document.getElementsByTagName('input')[0].onblur = function() {
socket.send(this.value);
};
</script>

2.)Nodejs socket 后台:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
var http = require("http");
var socket = require("socket.io");

// 启http服务
var server = http.createServer(function (req, res) {
res.writeHead(200, {
"Content-type": "text/html",
});
res.end();
});

server.listen("8080");
console.log("Server is running at port 8080...");

// 监听socket连接
socket.listen(server).on("connection", function (client) {
// 接收信息
client.on("message", function (msg) {
client.send("hello:" + msg);
console.log("data from client: ---> " + msg);
});

// 断开处理
client.on("disconnect", function () {
console.log("Client socket has closed.");
});
});


title: Vue.js基础

date: 2019-09-07 09:42:13

tags:  # 这里写的分类会自动汇集到 categories 页面上,分类可以多级

  • Vue.js # 一级分类
  • Vue.js基础 # 二级分类

介绍

Vue 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。

vue 和 react 都不是 mvvm 框架,只是借鉴

什么是 MVVM

MVVM 是 Model-View-ViewModel 的缩写。MVVM 是一种设计思想。Model 层代表数据模型,也可以在 Model 中定义数据修改和操作的业务逻辑;View 代表 UI 组件,它负责将数据模型转化成 UI 展现出来,ViewModel 是一个同步 View 和 Model 的对象。

在 MVVM 架构下,View 和 Model 之间并没有直接的联系,而是通过 ViewModel 进行交互,Model 和 ViewModel 之间的交互是双向的, 因此 View 数据的变化会同步到 Model 中,而 Model 数据的变化也会立即反应到 View 上。viewmodel 是组件的实例.

ViewModel 通过双向数据绑定把 View 层和 Model 层连接了起来,而 View 和 Model 之间的同步工作完全是自动的,无需人为干涉,因此开发者只需关注业务逻辑,不需要手动操作 DOM, 不需要关注数据状态的同步问题,复杂的数据状态维护完全由 MVVM 来统一管理。

Vue.js 的优点:

  1. 低耦合。视图(View)可以独立于 Model 变化和修改,一个 ViewModel 可以绑定到不同的”View”上,当 View 变化的时候 Model 可以不变,当 Model 变化的时候 View 也可以不变。
  2. 可重用性。你可以把一些视图逻辑放在一个 ViewModel 里面,让很多 view 重用这段视图逻辑。
  3. 独立开发。开发人员可以专注于业务逻辑和数据的开发(ViewModel),设计人员可以专注于页面设计。
  4. 可测试。界面素来是比较难于测试的,而现在测试可以针对 ViewModel 来写
  5. 易用灵活高效

引入

在 html 内引入:

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>

工程化时最好使用vue/cli构建.

实例和数据绑定

通过构造函数Vue就可以创建一个Vue的实例.并启动Vue应用.

1
2
3
4
let app = new Vue({
el: "#app",
data: { msg: "hello world" },
});

el用于指定页面中的 DOM 元素来挂载 Vue 实例.可以是标签,可以是 css 语法.

通过Vue实例的data选项,可以声明应用内可以双向绑定的数据.建议所有会用到的数据都预先在data内声明,也可以指向一个已有的变量.值可以先设置默认值.

挂载成功后,可以通过app.$el来访问该元素,Vue实例本身也代理了data对象里的所有属性,所以可以这样访问:

访问 Vue 实例的属性: app.$el, app.$data.

访问 data 元素的属性: app.msg

生命周期钩子

钩子(hook),可以理解为挂载点.在整个 Vue 实例创建过程中,有一些挂载点.

生命周期流程:

所有的生命周期钩子自动绑定this上下文到实例中,因此你可以访问数据,对属性和方法进行运算。这意味着你不能使用箭头函数来定义一个生命周期方法 (例如 created: () => this.fetchTodos())。这是因为箭头函数绑定了父上下文,因此 this 与你期待的 Vue 实例不同,this.fetchTodos 的行为未定义。

生命周期钩子 类型 详细
beforeCreate Function 在实例初始化之后,数据观测 (data observer) 和 event/watcher 事件配置之前被调用。
created Function 在实例创建完成后被立即调用。在这一步,实例已完成以下的配置:数据观测 (data observer),属性和方法的运算,watch/event 事件回调。然而,挂载阶段还没开始,$el属性目前不可见。
beforeMoute Function 在挂载开始之前被调用:相关的render函数首次被调用。
mounted Function el被新创建的vm.$el替换,并挂载到实例上去之后调用该钩子。如果 root 实例挂载了一个文档内元素,当mounted被调用时vm.$el也在文档内。
beforeUpdate Function 数据更新时调用,发生在虚拟 DOM 打补丁之前。这里适合在更新之前访问现有的 DOM,比如手动移除已添加的事件监听器。
updated Function 由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。当这个钩子被调用时,组件 DOM 已经更新,所以你现在可以执行依赖于 DOM 的操作。然而在大多数情况下,你应该避免在此期间更改状态。如果要相应状态改变,通常最好使用计算属性或watcher取而代之
actived Function keep-alive 组件激活时调用。
deactivated Function keep-alive 组件停用时调用。
beforeDestroy Function 实例销毁之前调用。在这一步,实例仍然完全可用。
destroyed Function Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。
errorCaptured (err: Error, vm: Component, info: string) => ?boolean 当捕获一个来自子孙组件的错误时被调用。此钩子会收到三个参数:错误对象、发生错误的组件实例以及一个包含错误来源信息的字符串。此钩子可以返回false以阻止该错误继续向上传播。

不要在选项属性或回调上使用箭头函数,比如 created: () => console.log(this.a) 或 vm.nextTick替换掉mounted`:

1
2
3
4
5
6
mounted: function () {
this.$nextTick(function () {
// Code that will run only after the
// entire view has been rendered
})
}

注意updated不会承诺所有的子组件也都一起被重绘。如果你希望等到整个视图都重绘完毕,可以用vm.$nextTick替换掉updated

1
2
3
4
5
6
updated: function () {
this.$nextTick(function () {
// Code that will run only after the
// entire view has been re-rendered
})
}

文本插值和表达式

使用双大括号(Mustache 语法)的方法是最基本的文本插值的语法,它会自动将双向绑定的数据实时显示出来.

1
<span>Message: {{ msg }}</span>

通过使用 v-once 指令,你也能执行一次性地插值,当数据改变时,插值处的内容不会更新。但请留心这会影响到该节点上的其它数据绑定:

1
<span v-once>这个将不会改变: {{ msg }}</span>

v-html

双大括号会将数据解释为普通文本,而非 HTML 代码。为了输出真正的 HTML,你需要使用 v-html 指令:

1
2
<p>Using mustaches: {{ rawHtml }}</p>
<p>Using v-html directive: <span v-html="rawHtml"></span></p>

你的站点上动态渲染的任意 HTML 可能会非常危险,因为它很容易导致 XSS 攻击。请只对可信内容使用 HTML 插值,绝不要对用户提供的内容使用插值。

v-bind

v-bind可缩写成:.

双大括号语法不能作用在 HTML 特性上,遇到这种情况应该使用 v-bind 指令:

1
<div v-bind:id="dynamicId"></div>

对于布尔特性 (它们只要存在就意味着值为 true),v-bind 工作起来略有不同,在这个例子中:

1
<button v-bind:disabled="isButtonDisabled">Button</button>

如果isButtonDisabled的值是null、undefinedfalse,则disabled特性甚至不会被包含在渲染出来的 <button> 元素中。

js 表达式

Vue .js 只支持单个表达式,不支持语句和流控制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
{
number + 1;
}
}
{
{
ok ? "YES" : "NO";
}
}
{
{
message.split("").reverse().join("");
}
}
<div v-bind:id="'list-' + id"></div>; //文本插值的形式,其中不能书写表达式,支持单个表达式

这些表达式会在所属 Vue 实例的数据作用域下作为 JavaScript 被解析。

有个限制就是,每个绑定都只能包含单个表达式,所以下面的例子都不会生效。

1
2
3
4
<!-- 这是语句,不是表达式 -->
{{ var a = 1 }}
<!-- 流控制也不会生效,请使用三元表达式 -->
{{ if (ok) { return message } }}

模板表达式都被放在沙盒中,只能访问全局变量的一个白名单,如 Math 和 Date 。你不应该在模板表达式中试图访问用户定义的全局变量。

计算属性

1
2
3
<div id="example">
{{ message.split('').reverse().join('') }}
</div>

这里是想要显示变量 message 的翻转字符串,但是更复杂的就需要计算属性.

所有的计算属性都以函数的形式写在 Vue 实例内的computed选项内,最终返回计算后的结果。

范例:

1
2
3
4
<div id="example">
<p>message: "{{ message }}"</p>
<p>reversed message: "{{ reversedMessage }}</p>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var vm = new Vue({
el: '#example',
data: {
message: 'hello'
},
computed: {
//计算属性的getter
reversedMessage: function(){
//this指向vm实例
return this.message.split('').reverse.().join('')
}
}
})
//结果:
//message: "hello"
//reversed message: "olleh"

计算属性缓存

1
2
3
4
5
6
// 在组件中
methods: {
reversedMessage: function () {
return this.message.split('').reverse().join('')
}
}

使用 methods 方法也可以起到和计算属性同样效果

区别:

methods: 如果调用方法,只要页面重新渲染.方法就会重新执行,不需要渲染就不需要重新执行

计算属性: 不管渲不渲染,只要计算属性依赖的数据不变,就不会变

何时使用: 取决于是否需要缓存,当遍历大量数组或大量计算,应该使用计算属性.

计算属性缓存 vs 方法 methods

我们可以将同一函数定义为一个方法而不是一个计算属性。两种方式的最终结果确实是完全相同的。然而,不同的是计算属性是基于它们的响应式依赖进行缓存的。只在相关响应式依赖发生改变时它们才会重新求值。这就意味着只要 message 还没有发生改变,多次访问 reversedMessage 计算属性会立即返回之前的计算结果,而不必再次执行函数。

这也同样意味着下面的计算属性将不再更新,因为 Date.now() 不是响应式依赖:

1
2
3
4
5
computed: {
now: function () {
return Date.now()
}
}

相比之下,每当触发重新渲染时,调用方法 methods 将总会再次执行函数。

计算属性 vs 侦听属性 watch

watch:主要用来监听数据变化.可以监听:props,data,computed 内的数据.

属于我的变化影响别人.

计算属性 computed: 依赖 data 中的数据,只有该依赖的数据发生变化才会变化.

属于别人的变化影响我.

Vue 提供了一种更通用的方式来观察和响应 Vue 实例上的数据变动:侦听属性。当你有一些数据需要随着其它数据变动而变动时,你很容易滥用 watch——特别是如果你之前使用过 AngularJS。然而,通常更好的做法是使用计算属性而不是命令式的 watch 回调。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<div id="demo">{{ fullName }}</div>;
var vm = new Vue({
el: "#demo",
data: {
firstName: "Foo",
lastName: "Bar",
fullName: "Foo Bar",
},
watch: {
firstName: function (val) {
this.fullName = val + " " + this.lastName;
},
lastName: function (val) {
this.fullName = this.firstName + " " + val;
},
},
});

计算属性版本:

1
2
3
4
5
6
7
8
9
10
11
12
var vm = new Vue({
el: "#demo",
data: {
firstName: "Foo",
lastName: "Bar",
},
computed: {
fullName: function () {
return this.firstName + " " + this.lastName;
},
},
});

计算属性的 setter

计算属性默认只有getter,不过在需要时你也可以提供一个setter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ...
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]
}
}
}
// ...

现在再运行 vm.fullName = ‘John Doe’ 时,setter 会被调用,vm.firstName 和 vm.lastName 也会相应地被更新。

侦听器 watch

当需要在数据变化时执行异步或开销较大的操作时,更适合用 watch.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<div id="watch-example">
<p>
Ask a yes/no question:
<input v-model="question">
</p>
<p>{{ answer }}</p>
</div>
<script src="https://cdn.jsdelivr.net/npm/axios@0.12.0/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lodash@4.13.1/lodash.min.js"></script>
<script>
var watchExampleVM = new Vue({
el: '#watch-example',
data: {
question: '',
answer: 'I cannot give you an answer until you ask a question!'
},
watch: {
// 如果 `question` 发生改变,这个函数就会运行
question: function (newQuestion, oldQuestion) {
this.answer = 'Waiting for you to stop typing...'
this.debouncedGetAnswer()
}
},
created: function () {
// `_.debounce` 是一个通过 Lodash 创建的函数防抖.
this.debouncedGetAnswer = _.debounce(this.getAnswer, 500)
},
methods: {
getAnswer: function () {
//如果要检索的字符串值没有出现,则indexOf返回 -1。
if (this.question.indexOf('?') === -1) {
this.answer = 'Questions usually contain a question mark. ;-)'
return
}
this.answer = 'Thinking...'
var vm = this
axios.get('https://yesno.wtf/api')
.then(function (response) {
vm.answer = _.capitalize(response.data.answer)
})
.catch(function (error) {
vm.answer = 'Error! Could not reach the API. ' + error
})
}
}
})
</script>

在这个示例中,使用 watch 选项允许我们执行异步操作 (访问一个 API),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。

v-bind 与 class 和 style 的绑定

应用场景: dom 元素经常绑定一些 class 类名或 style 样式

对象语法

可以给v-bind:class传一个对象,动态切换 class:

1
<div v-bind:class="{ active: isActive }"></div>

也可以传入多个属性来动态切换 class.v-bind:class指令也可以与普通 class 属性共存.

绑定的数据对象也可以写在data里.

1
2
3
4
5
6
7
8
9
10
<div v-bind:class="classObject"></div>

<script>
data: {
classObject: {
active: true,
'text-danger': false
}
}
</script>

当 class 的表达式过长或者逻辑复杂,可以绑定一个计算属性.一般当条件多于两个时,就可以使用datacomputed

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<div v-bind:class="classObject"></div>
<script>
data: {
isActive: true,
error: null
},
computed: {
classObject: function () {
return {
active: this.isActive && !this.error,
'text-danger': this.error && this.error.type === 'fatal'
}
}
}
</script>

数组语法

当应用多个 class 时,可以使用数组语法,给v-bind:class绑定一个数组.数组成员直接对应类名.

1
2
3
4
5
6
7
<div v-bind:class="[activeClass, errorClass]"></div>
<script>
data: {
activeClass: 'active',
errorClass: 'text-danger'
}
</script>

渲染为

1
<div class="active text-danger"></div>

根据条件切换,也可以使用三元表达式:

1
<div v-bind:class="[isActive ? activeClass : '', errorClass]"></div>

也可以数组语法,对象语法混用:

1
<div v-bind:class="[{ active: isActive }, errorClass]"></div>

绑定内联样式

对象语法

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

1
2
3
4
5
6
7
<div v-bind:style="{color: activeColor, fontSize: fontSize + 'px' }"></div>
<script>
data: {
color: 'red',
fontSize: 30
}
</script>

同样的,对象语法常常结合返回对象的计算属性使用。

数组语法

数组语法可以将多个样式对象应用到同一个元素上:

1
<div v-bind="[baseStyles, overridingStyles]"></div>

应用多个样式对象时,可以使用数组语法.在实际业务中,style 的数组语法并不常用,因为往往可以写在一个对象里面.而较为常用的应当是计算属性.

使用:style时,Vue 会自动给特殊的 css 属性名称增加前缀,比如 transform,无需再加前缀属性.

基本指令

v-cloak

解决因为初始化慢,导致页面闪动的情况.这个指令可以隐藏未编译的 Mustache 标签直到实例准备完毕.

一般和CSS规则[v-cloak] { display: none }配合使用(需要写到 css 里).

v-once

只渲染元素和组件一次.之后的重新渲染被当做静态内容跳过,用于优化性能.

写在标签里.

v-pre

跳过这个元素和他子元素的编译过程.可以加快编译.

其他的单独写

v-text, v-html, v-show, v-if, v-else, v-else-if, v-for, v-on, v-bind, v-model, v-slot.

条件渲染

v-if,v-else,v-else-if

用法: 根据表达式值的真假条件渲染元素.在切换时元素及它的数据绑定/组件被销毁并重建.

如果元素是<template>,将提出他的内容作为条件块.

当条件变化时,该指令触发过渡效果.

当和v-for一起使用时,v-for的优先级更高.

也可以用 v-else 添加一个“else 块”:

1
2
<h1 v-if="awesome">Vue is awesome!</h1>
<h1 v-else>Oh no 😢</h1>

<template>上使用v-if

因为v-if是一个指令,所以必须将它添加到一个元素上。但是如果想切换多个元素呢?此时可以把一个<template>元素当做不可见的包裹元素,并在上面使用v-if。最终的渲染结果将不包含<template>元素。

1
2
3
4
5
<template v-if="ok">
<h1>Title</h1>
<p>Paragraph 1</p>
<p>Paragraph 2</p>
</template>

v-else-if,顾名思义,充当 v-if 的“else-if 块”,可以连续使用:

1
2
3
4
5
6
7
8
9
10
11
12
<div v-if="type === 'A'">
A
</div>
<div v-else-if="type === 'B'">
B
</div>
<div v-else-if="type === 'C'">
C
</div>
<div v-else>
Not A/B/C
</div>

key管理可复用元素

Vue 会尽可能高效地渲染元素,通常会复用已有元素而不是从头开始渲染.<input>不会被替换掉——仅仅是替换了它的 placeholder

如果要表达“这两个元素是完全独立的,不要复用它们”。只需添加一个具有唯一值的key属性即可.

1
2
3
4
5
6
7
8
<template v-if="loginType === 'username'">
<label>Username</label>
<input placeholder="Enter your username" key="username-input">
</template>
<template v-else>
<label>Email</label>
<input placeholder="Enter your email address" key="email-input">
</template>

v-show

1
<h1 v-show="ok">Hello!</h1>

v-show只是切换 css 属性display.v-show不支持<template>元素,也不支持v-else.

v-if 和 v-show 相比

v-if: 真正的条件渲染,条件为真就渲染,为 false 就移除.

v-show: 不管初始条件是什么,都会渲染.只是切换 css 的 display 属性.

总结: 如果需要非常频繁地切换,则使用v-show较好;如果在运行时条件很少改变,则使用v-if较好

v-for 列表渲染

两种使用场景:

  1. 遍历多个对象
  2. 遍历一个对象的多个属性

遍历数组语法: v-for="(item, index) in items"

第一个是数组元素名,第二个是索引.

遍历对象的多个属性语法: v-for="(value, name, index) in object"

第一个是属性值,第二个是属性名,第三个是索引.

在遍历对象时,会按 Object.keys() 的结果遍历,但是不能保证它的结果在不同的 JavaScript 引擎下都一致。

为了给 Vue 一个提示,以便它能跟踪每个节点的身份,从而重用和重新排序现有元素,你需要为每项提供一个唯一 key 属性:

1
2
3
<div v-for="item in items" v-bind:key="item.id">
<!-- 内容 -->
</div>

建议尽可能在使用 v-for 时提供 key attribute,除非遍历输出的 DOM 内容非常简单,或者是刻意依赖默认行为以获取性能上的提升。

不要使用对象或数组之类的非基本类型值作为 v-for 的 key。请用字符串或数值类型的值。

Vue 中的 key

当 Vue.js 用 v-for 正在更新已渲染过的元素列表时,它默认用“就地复用”策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序, 而是简单复用此处每个元素,并且确保它在特定索引下显示已被渲染过的每个元素。

这里的就地复用的策略复用的是没有发生改变的元素,其他的还要依次重排。

所以我们需要使用 key 来给每个节点做一个唯一标识,Vue 的 Diff 算法就可以正确的识别此节点,找到正确的位置区插入新的节点,所以一句话,key 的作用主要是为了高效的更新虚拟 DOM.

v-for 和 v-if

当它们处于同一节点,v-for 的优先级比 v-if 更高,这意味着 v-if 将分别重复运行于每个v-for 循环中。当你只想为部分项渲染节点时,这种优先级的机制会十分有用,如下:

1
2
3
<li v-for="todo in todos" v-if="!todo.isComplete">
{{ todo }}
</li>

上面的代码将只渲染未完成的 todo。

而如果你的目的是有条件地跳过循环的执行,那么可以将 v-if 置于外层元素 (或 )上。如:

1
2
3
4
5
6
<ul v-if="todos.length">
<li v-for="todo in todos">
{{ todo }}
</li>
</ul>
<p v-else>No todos left!</p>

在组件上使用 v-for

当在组件上使用 v-for 时,key 现在是必须的.

然而,任何数据都不会被自动传递到组件里,因为组件有自己独立的作用域。为了把迭代数据传递到组件里,我们要使用 prop,

不自动将 item 注入到组件里的原因是,这会使得组件与 v-for的运作紧密耦合。明确组件数据的来源能够使组件在其他场合重复使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//html
<div id="todo-list-example">
<form v-on:submit.prevent="addNewTodo">
<label for="new-todo">Add a todo</label>
<input
v-model="newTodoText"
id="new-todo"
placeholder="E.g. Feed the cat"
>
<button>Add</button>
</form>
<ul>
<li
is="todo-item"
v-for="(todo, index) in todos"
v-bind:key="todo.id"
v-bind:title="todo.title"
v-on:remove="todos.splice(index, 1)"
></li>
</ul>
</div>

注意这里的 is="todo-item"属性。这种做法在使用 DOM 模板时是十分必要的,因为在 <ul> 元素内只有<li> 元素会被看作有效内容。这样做实现的效果与 <todo-item> 相同,但是可以避开一些潜在的浏览器解析错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
//js
Vue.component("todo-item", {
template:
"\
<li>\
{{ title }}\
<button v-on:click=\"$emit('remove')\">Remove</button>\
</li>\
",
props: ["title"],
});

new Vue({
el: "#todo-list-example",
data: {
newTodoText: "",
todos: [
{
id: 1,
title: "Do the dishes",
},
{
id: 2,
title: "Take out the trash",
},
{
id: 3,
title: "Mow the lawn",
},
],
nextTodoId: 4,
},
methods: {
addNewTodo: function () {
this.todos.push({
id: this.nextTodoId++,
title: this.newTodoText,
});
this.newTodoText = "";
},
},
});

数组更新过滤排序

改变数组的一系列方法:

  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()

两个数组变动 Vue 检测不到:

  1. 改变数组指定项,vm.items[indexOfItem] = newValue
  2. 改变数组长度,vm.items.length = newLength

解决办法:

改变指定项: Vue.set(app.arr, 1, ‘car’)

改变数组长度: app.arr.splice(1)

显示过滤/排序后的结果

有时,我们想要显示一个数组经过过滤或排序后的版本,而不实际改变或重置原始数据。在这种情况下,可以创建一个计算属性,来返回过滤或排序后的数组。

计算属性

1
2
3
4
5
6
7
8
9
10
<li v-for="n in evenNumbers">{{ n }}</li>
data: {
numbers: [ 1, 2, 3, 4, 5 ]
},
computed: {
evenNumbers: function () {
return this.numbers.filter(function (number) {
return number % 2 === 0
})
}

使用 methods

1
2
3
4
5
6
7
8
9
10
11
<li v-for="n in even(numbers)">{{ n }}</li>
data: {
numbers: [ 1, 2, 3, 4, 5 ]
},
methods: {
even: function (numbers) {
return numbers.filter(function (number) {
return number % 2 === 0
})
}
}

v-on

v-on用来绑定事件监听器.v-on可以缩写成@.

在普通元素上,v­-on可以监听原生的 DOM 事件,除了click外,还有dblclickkeyup,mousemove等。

表达式可以是一个方法名,这些方法都写在 Vue 实例的 methods 属性内,并且是函数的形式,

函数内的this指向的是当前 Vue 实例本身,因此可以直接使用this.xxx的形式来访问或修改数据.

如果方法中带有参数,但是没有加括号,默认传原生事件对象 event

动态参数

从 2.6.0 开始,可以用方括号括起来的 JavaScript 表达式作为一个指令的参数:

<a v-bind:[attributeName]="url"> ... </a>

方括号里的属性可以被替换.

可以使用动态参数为一个动态的事件名绑定处理函数:

<a v-on:[eventName]="doSomething"> ... </a>

eventName的值为 “focus” 时,v-on:[eventName]将等价于 v-on:focus

动态参数表达式有一些语法约束,因为某些字符,例如空格和引号,放在 HTML 特性名里是无效的。同样,在 DOM 中使用模板时你需要回避大写键名。

修饰符

在 Vue 中传入 event 对象用$event

修饰符 (modifier) 是以半角句号 . 指明的特殊后缀,用于指出一个指令应该以特殊方式绑定。

.stop 阻止单击事件冒泡 <a v-on:click.stop="doThis"></a>

.prevent 提交事件不再重载页面

.capture 添加事件侦听器时使用时间捕获模式

.self 只当事件在该元素本身(而不是子元素)触发时触发回调

.once只执行一次的方法

.passive

Vue 还对应 addEventListener 中的 passive 选项提供了 .passive修饰符。

1
2
3
4
<!-- 滚动事件的默认行为 (即滚动行为) 将会立即触发 -->
<!-- 而不会等待 `onScroll` 完成 -->
<!-- 这其中包含 `event.preventDefault()` 的情况 -->
<div v-on:scroll.passive="onScroll">...</div>

这个 .passive 修饰符尤其能够提升移动端的性能。

不要把 .passive.prevent 一起使用,因为 .prevent将会被忽略,同时浏览器可能会向你展示一个警告。请记住,.passive 会告诉浏览器你不想阻止事件的默认行为。

使用修饰符时,顺序很重要;相应的代码会以同样的顺序产生。因此,用 v-on:click.prevent.self 会阻止所有的点击,而 v-on:click.self.prevent 只会阻止对元素自身的点击。

按键修饰符

1
2
<!-- 只有在 `key``Enter` 时调用 `vm.submit()` -->
<input v-on:keyup.enter="submit">

你可以直接将 KeyboardEvent.key 暴露的任意有效按键名转换为 kebab-case 来作为修饰符。

1
<input v-on:keyup.page-down="onPageDown">

在上述示例中,处理函数只会在 $event.key 等于 PageDown 时被调用。

系统修饰键

.ctrl

.alt

.shift

.meta

.exact

.exact 修饰符允许你控制由精确的系统修饰符组合触发的事件。

1
2
3
4
5
6
7
8
<!-- 即使 AltShift 被一同按下时也会触发 -->
<button @click.ctrl="onClick">A</button>

<!-- 有且只有 Ctrl 被按下的时候才触发 -->
<button @click.ctrl.exact="onCtrlClick">A</button>

<!-- 没有任何系统修饰符被按下的时候才触发 -->
<button @click.exact="onClick">A</button>

v-model

用于在表单类元素上双向绑定

可以用于 input 框,以及 textarea 等

注意:所显示的值只依赖于所绑定的数据,不再关心初始化时的插入的 value

v-model的实际

v-model其实是一个语法糖,其实绑定了两层操作:

  1. v-bind绑定一个 value 值
  2. v-on指令给当前元素绑定 input 事件
1
2
3
4
5
6
<input v-model="total">
//等价于
<input
v-bind:value="total"
v-on:input="total = $event.target.value"
>

要使用v-model,要做到:

  1. 接收一个 value 属性
  2. 在有新的 value 时触发 input 事件

文本

1
2
<input v-model="message" placeholder="edit me">
<p>Message is: {{ message }}</p>

多行文本

v-model绑定到textarea

1
2
3
4
5
<span>Multiline message is:</span>
<p style="white-space: pre-line;">{{ message }}</p>


<textarea v-model="message" placeholder="add multiple lines"></textarea>

单选按钮

  1. 单个单选按钮,直接用 v-­bind 绑定一个布尔值,用 v-­model 是不可以的
  2. 如果是组合使用,就需要 v-­model 来配合 value 使用,绑定选中的单选框的 value 值.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<div id="example-4">
<input type="radio" id="one" value="One" v-model="picked">
<label for="one">One</label>


<input type="radio" id="two" value="Two" v-model="picked">
<label for="two">Two</label>


<span>Picked: {{ picked }}</span>
</div>
<script>
new Vue({
el: '#example-4',
data: {
picked: ''
}
})
</script>

复选框

单个复选框,绑定到布尔值:

1
2
<input type="checkbox" id="checkbox" v-model="checked">
<label for="checkbox">{{ label }}</label>

多个复选框,绑定到同一个数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<div id='example-3'>
<input type="checkbox" id="jack" value="Jack" v-model="checkedNames">
<label for="jack">Jack</label>
<input type="checkbox" id="john" value="John" v-model="checkedNames">
<label for="john">John</label>
<input type="checkbox" id="mike" value="Mike" v-model="checkedNames">
<label for="mike">Mike</label>


<span>Checked names: {{ checkedNames }}</span>
</div>
<script>
new Vue({
el: "#example-3",
data: {
checkedNames: []
}
})
</script>

下拉框

单选下拉:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<div id="example-5">
<select v-model="selected">
// 将v-model绑定到select上,data中selected使用空字符串
<option disabled value="">请选择</option>
<option>A</option>
<option>B</option>
<option>C</option>
</select>
<span>Selected: {{ selected }}</span>
</div>

<script>
new Vue({
el: "#example-5",
data: {
selected: ''
}
})
</script>

多选下拉:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<div id="example-6">
<select v-model="selected" multiple style="width: 50px">
// 将v-model绑定到select上,data中selected使用空数组
<option>A</option>
<option>B</option>
<option>C</option>
</select>
<span>Selected: {{ selected }}</span>
</div>

<script>
new Vue({
el: '#example-6',
data: {
selected: []
}
})
</script>

使用v-for配合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<select v-model="selected">
<option v-for="option in options" v-bind:value="option.value">
{{ option.text }}
</option>
</select>
<span>Selected: {{ selected }}</span>

<script>
new Vue({
el: "...",
data: {
selected: 'A',
options: [
{ text: 'One', value: 'A' },
{ text: 'Two', value: 'B' },
{ text: 'Three', value: 'C' }
]
}
})
</script>

绑定值

  • 单选按钮

只需要用 v-­bind 给单个单选框绑定一个 value 值,此时,v­-model 绑定的就是他的 value 值

  • 复选框
  • 下拉框

在 select 标签上绑定 value 值对 option 并没有影响

修饰符

修饰符 解释
lazy v-­model 默认是在 input 输入时实时同步输入框的数据,而 lazy 修饰符,可以使其在失去焦点或者敲回车键之后再更新
number 将输入的字符串转化为 number 类型
trim trim 自动过滤输入过程中首尾输入的空格


title: CSS

date: 2019-08-23 16:54:50

tags: css

categories:  # 这里写的分类会自动汇集到 categories 页面上,分类可以多级

  • css # 一级分类
  • css3 # 二级分类

介绍

css,即层叠样式表,可以为网页添加样式.

stylesheet

应用方式

外部样式表

  1. 通过<link>引入

<link rel="stylesheet" href="index.css"> 2. 通过@import引入,注意末尾加;号,CSS 语法,只能在 CSS 内使用.

1
<style>@import url("index.css"); @import "index.css";</style>
  1. 媒体查询(响应式)
1
2
3
4
5
6
7
8
<style>
@media (min-width: 801px) {
body {
margin: 0 auto;
width: 800px;
}
}
</style>

内部样式表

将 css 放到<style>元素中,一般放到文档的<head>中.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<title>CSS</title>
<style>
h1 {
background: red;
}
</style>

</head>
<body>
<h1>AMD yes!</h1>
</body>
</html>

内联样式

<p style="background: red; font-size: 20px;">AMD yes!<p>

CSS 选择器

基本选择器 名称 含义
* 通用选择器 匹配任何元素
E 标签选择器 匹配所有使用 E 标签的元素
.info class 选择器 匹配所有 class 属性中包含 info 的元素
#footer id 选择器 匹配所有 id 属性等于 footer 的元素
组合选择器 名称 含义
E,F 多元素选择器 同时匹配所有 E 元素或 F 元素
E F 后代选择器 匹配所有属于 E 元素后代的 F 元素
E>F 子元素选择器 匹配所有 E 元素的直接子元素
E+F 直接相邻元素选择器 匹配 E 元素后的直接相邻元素
E~F 兄弟元素选择器 匹配 E 元素后的所有兄弟元素
属性选择器 含义
E[attr] 匹配所有具有 attr 属性的 E 元素
E[attr=val] 匹配所有 attr 属性等于 val 的 E 元素
E[attr~=val] 匹配所有 attr 属性具有多个空格分隔的值,其中一个等于 val 的 E 元素
E[attr =val]
E[attr*=val] 匹配所有 attr 属性中包含 val 的 E 元素
E[attr^=val] 匹配所有 attr 属性中以 val 开头的 E 元素
E[attr$=val] 匹配所有 attr 属性中以 val 结尾的 E 元素
伪类 含义
E:first-child 匹配父元素的第一个子元素
E:link 匹配所有未被点击的链接
E:visited 匹配所有已被点击的链接
E:active 匹配鼠标已经按下没有释放的 E 元素
E:hover 匹配鼠标悬停其上的 E 元素
E:focus 匹配获得焦点的 E 元素
E:enabled 匹配表单中激活的元素
E:disabled 匹配表单中禁用的元素
E:checked 匹配表单中被选中的 radio 或者 checkbox 元素
E:root 匹配文档的根元素,对于 html 文档,就是 html 元素
E:nth-child(n) 匹配其父元素的第 n 个子元素,第一个编号为 1
E:nth-last-child(n) 匹配其父元素的倒数第 n 个子元素,第一个编号为 1
E:nth-of-type 与 nth-child 类似,但仅匹配使用同种标签的元素
E:nth-last-child 与 nth-last-child 类似,但仅匹配使用同种标签的元素
E:last-child 匹配父元素的最后一个元素
E:first-of-type 匹配父元素下使用同种标签的第一个子元素
E:last-of-type 匹配父元素下使用同种标签的最后一个子元素
E:only-child 匹配父元素下仅有的一个子元素,等同于:first-child:last-child 或 :nth-child(1):nth-last-child(1)
E:only-of-type 匹配父元素下使用同种标签的唯一一个子元素,等同于:first-of-type:last-of-type 或 :nth-of-type(1):nth-last-of-type(1)
E:empty 匹配一个不包含任何子元素的元素,注意,文本节点也被看做子元素
E:not(s) 匹配不符合当前选择器的任何元素
E:target 代表一个唯一的页面元素(目标元素),其 id 与当前 URL 片段匹配
伪元素 含义
E::first-line 匹配 E 元素的第一行
E::first-letter 匹配 E 元素的第一个字母
E::before 在 E 元素之前插入生成的内容
E::after 在 E 元素之后插入生成的内容
E::selection 匹配用户当前选中的元素

伪元素必须要有content

伪元素和伪类的区别

  1. 伪元素使用两个冒号,伪类使用一个冒号
  2. 伪元素添加了一个页面中没有的元素(只是从视觉效果上添加了,不是在文档树中添加),伪类是给页面中已经存在的的元素添加一个类.

CSS 选择器有哪些?哪些属性可以继承?

1.id 选择器( # myid)

2.类选择器(.myclassname)

3.标签选择器(div, h1, p)

4.相邻选择器(h1 + p)

5.子选择器(ul > li)

6.后代选择器(li a)

7.通配符选择器( * )

8.属性选择器(a[rel = “external”])

9.伪类选择器(a:hover, li:nth-child)

可继承的样式: font-size font-family color, ul li dl dd dt;

不可继承的样式:border padding margin width height;

选择器优先级

!important优先级最高(由于 IE 不支持!important,所以也可以利用它区分不同的浏览器。)

同权重: 内联样式表(标签内部)> 嵌入样式表(当前文件中)> 外部样式表(外部文件中)

行内样式(写在 style 标签里的样式) > id 样式 > class 样式 > 标签名样式

CSS 基本样式

div(块级元素):高度是由内部文档流元素高度的总和决定的。文档流:文档内元素的流动方向,内联元素是从左往右,块级元素是从上往下

span(内联元素):高度是由其中文字高度决定的,内联元素设置 width 和 height 是无效的,上下的 margin 和 padding 也无效,要将它们设为display:inline-block才有效。

  • 常见块级元素: div h1~h6 p hr form ul ol li table td tr th dl dt dd pre
  • 常见内联元素: em strong span a br img button input label select textarea code script

尽量不写 height 和 width,这两个属性会引出很多 bug,要宽高的时候可以用 padding,但 img 最好先写 width,因为可以先占位,因为引用图片时浏览器不知道图片大小,所有等图片下载完成,它后面的元素又要重新排位置,若先写好 width,则不用重排,知道 height 也可以先写好 height。

另外 span 元素设置 padding 的时候要将它设为 display:inline-block,因为内联元素不能设置宽高,inline-block 具有 inline 的同行特性,也具有 block 的高度特性。

对于 display:inline(内联元素)的元素,设置 width/height/上下 margin 和 padding 都是无效的

inline-block

  1. inline-block 之间空隙
  • inline-block 之间有空格、Tab、换行符。
  • 给父元素设置 font-size: 0,在 inline-block 元素上重新设置 font-size。
  1. inline-block 导致父元素增高若干像素
  • 给 inline-block 元素设置 vertical-align: top

通用解决办法 不要设置 inline-block,使用 float 或 flex。

浏览器默认样式

浏览器对某些元素设置有默认样式,如 h1, ul, li 等。

常见处理方式

  1. Normalize.css
  2. CSS Reset
  3. 简单去除
1
2
3
4
* {
margin: 0;
padding: 0;
}

CSS 常见样式

边框 border

1
2
3
4
5
6
7
8
9
10
11
.box {
border-width: 1px;
border-color: red;
border-style: solid;
border-bottom: none;//下边框消失
border-radius: 50%;//圆角
}
//简写
.box2 {
border: 1px red solid;
}

实现不使用 border 画出 1px 高的线,在不同浏览器的标准模式与怪异模式下都能保持一致的效果。

<div style="height:1px;overflow:hidden;background:red"></div>

内边距 padding

padding: 10px 20px 30px;//按照上右下左,即顺时针,缺哪个补哪个,缺左边,按20px补上.

外边距 margin

margin 可以合写,可以分开.可以是数值,可以是百分比(相对于父元素).还可以是负值.有外边距合并问题

display

  • 块级: block,list-item,table
  • 行内: inline,inline-table,inline-block
  • inherit: 规定应该从父元素继承 display 属性的值

display:inline-block 什么时候会显示间隙

移除空格、使用 margin 负值、使用 font-size:0、letter-spacing、word-spacing

font

1
2
3
4
5
6
7
8
9
body{
font: 12px/1.5 Arial;
}
p{
line-height: 1.5;//行高是字体的1.5倍
font-size: 14px;//字体大小
font-family: Arial;//字体
font-weight: bold;//文字粗度,粗体
}

Chrome 默认字体是 16px,最小字体是 12px.

文本

  • text-aglin: 文本对齐方式,left,right,center,justify
  • text-ident: 文案第一行缩进距离
  • text-decoration: 划线,none,underline,line-through,overline
  • text-transform: 改变文字大小写,none,uppercase,lowercase,captialize
  • word-spacing: 可以改变字(单词)之间的标准间距
  • letter-spacing: 字母之间的间隔

单行文本溢出加…

1
2
3
4
5
.box>h3 {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

颜色

  • 单词: red,blue,black,pink,yellow
  • 十六进制: #000000(黑),#fff(白),#eee(淡灰),#ccc(灰色),#666(深灰)
  • rgb:rgb(255,255,255)白色,rgba(0,0,0,0.5)透明度为 0.5 的黑色

单位

px rpx em rem的区别

  1. px是像素 pixel,相对于屏幕分辨率的单位
  2. em是相对单位,相对于当前对象内的文本尺寸。如果没有设置,则相对浏览器默认尺寸 16px,1em=16px。

为了简化,可以将 body 里 font-size=62.5%,那么 em 就是 16px*62%=10px。1em=10px

em 特点:em 值不固定,会继承父元素字体大小 3. rem是 root em。只相对于 html 根元素 4. rpx是微信小程序的方案,是响应式像素,可以根据屏幕宽度进行自适应。规定屏幕为 750rpx.物理像素

1px=2rpx 5. vh vw: 相对单位,1vw 为屏幕宽度的 1%

background

  • background-color: transparent 透明背景;颜色值;半透明浮层的背景颜色建议使用 rgba() 而不是 opacity 设置透明度。
  • background-image: url(../images/bg.png);// 属性用于给元素设置一张或多张背景图。
  • background-repeat 控制背景图片的重复方式。no-repeat不重复;repeat-x水平方向重复;repeat-y 垂直方向重复;
  • background-position 设置背景图的位置。x y;x% y%;[top|center|bottom][left|center|right]
  • background-size 设定背景图片的大小。IE9 以下不支持;cover缩放背景图片以完全覆盖背景区,可能背景图片部分看不见;contain缩放背景图片以完全装入背景区,可能背景区部分空白。
  • background-clip: 设置元素背景区域覆盖的范围.border-box 覆盖至边框的最外围;padding-box 覆盖至内边距的最外围;content-box 仅覆盖元素内容区域

缩写:

1
2
3
.avatar {
background: #fff url(avatar.svg) no-repeat left center;
}

隐藏 or 透明

  • opacity: 0;//透明度为 0,整体
  • visibility: hidden;//和 opacity: 0;类似
  • display: none;//消失,不占用位置
  • background-color: rgba(0,0,0,0.2);//只是背景色透明

display:none 和 visibility:hidden 的区别

display:none:

不占据任何空间,在文档渲染时,该元素如同不存在(但依然存在文档对象模型树中).会触发 reflow(回流),进行渲染.不是继承属性,元素及其子元素都会消失

visibility:

该元素空间依旧存在.只会触发 repaint(重绘),因为没有发现位置变化,不进行渲染.是继承属性,若子元素使用了 visibility:visible,则不继承,这个子孙元素又会显现出

box-shadow

box-shadow: -16px 0 16px 1px rgba(102,102,102,0.4);

第一个值代表阴影左右偏移,正数往右,负数往左

第二个值代表上下偏移,正往下,负往上

第三个值越大,模糊面积越大越淡

第四个值取正值时,阴影扩大,取负值时,阴影收缩

line-height

  • line-height: 2;//本身字体高度的 2 倍
  • line-height: 200%;//父元素高度的 2 倍,父元素没写行高就是默认 16px*2=32px
  • height=line-height;//垂直居中单行文本

盒模型

  • 标准盒模型: border-box,即 padding,border 不在 width,height 范围内
  • IE 盒模型: content-box,即 width=content 尺寸+padding+border

css3 可以设置box-sizing: content-box;//标准盒模型

position

CSS position 属性用于指定一个元素在文档中的定位方式。

语法: static | relative | absolute | sticky | fixed

  1. static: 该关键字指定元素使用正常的布局行为,即元素在文档常规流中当前的布局位置。此时 top, right, bottom, left 和 z-index 属性无效。
  2. relative: 该关键字下,元素先放置在未添加定位时的位置,再在不改变页面布局的前提下调整元素位置(因此会在此元素未添加定位时所在位置留下空白)。position:relative 对 table 元素无效.
  3. absolute: 不为元素预留空间,通过指定元素相对于最近的非 static 定位父元素的偏移,来确定元素位置。绝对定位的元素可以设置外边距(margins),且不会与其他边距合并。
  4. fixed: 不为元素预留空间,而是通过指定元素相对于屏幕视口(viewport)的位置来指定元素位置。元素的位置在屏幕滚动时不会改变。fixed 属性会创建新的层叠上下文。当元素祖先的 transform   属性非 none 时,容器由视口改为该祖先。
  5. sticky: 盒位置根据正常流计算(这称为正常流动中的位置),然后相对于该元素在流中的 flow root(BFC)和 containing block(最近的块级祖先元素)定位。在所有情况下(即便被定位元素为 table 时),该元素定位均不对后续元素造成影响。当元素 B 被粘性定位时,后续元素的位置仍按照 B 未定位时的位置来确定。

相对定位(relative)

相对定位的元素是在文档中的正常位置偏移给定的值,但是不影响其他元素的偏移。

绝对定位(absolute,fixed)

相对定位的元素并未脱离文档流,而绝对定位的元素则脱离了文档流。在布置文档流中其它元素时,绝对定位元素不占据空间。绝对定位元素相对于最近的非 static 祖先元素定位。当这样的祖先元素不存在时,则相对于 ICB(inital container block, 初始包含块)。

固定定位(fixed)

固定定位与绝对定位相似,但元素的包含块为 viewport 视口。该定位方式常用于创建在滚动屏幕时仍固定在相同位置的元素。

粘性定位(sticky)

粘性定位可以被认为是相对定位和固定定位的混合。元素在跨越特定阈值前为相对定位,之后为固定定位。

#one { position: sticky; top: 10px; }

在 viewport 视口滚动到元素 top 距离小于 10px 之前,元素为相对定位。之后,元素将固定在与顶部距离 10px 的位置,直到 viewport 视口回滚到阈值以下。(理解为大于 10px 时,属于相对定位,可以在页面滚动时随之滚动,当距视口 10px 时,固定了,就像粘在那里了)

浮动

float: left;//向左浮动

  • 浮动元素并不是完全意义上的脱离文档流,至少普通元素里的文本可以发现,行内元素也可以发现
  • 设置浮动会让块级元素从左到右或从右到左排列.
  • 块级元素设置浮动呈现 inline-block 特性,不再撑开父元素,宽度也会收缩
  • 行内元素设置浮动呈现 inline-block 特性,可以设置宽高.

清除浮动

问题:

  • 对后续元素位置产生影响
  • 父容器高度计算出现问题

清除浮动是为了清除使用浮动元素产生的影响。浮动的元素,高度会塌陷,而高度的塌陷使我们页面后面的布局不能正常显示。

通用解决方案

1
2
3
4
5
6
7
8
9
10
父元素加clearfix
.clearfix::after{
content:'';
display: block;
clear: both;
}
.clearfix{
*zoom: 1
//兼容IE6,7
}

BFC(块级格式化上下文)

决定了其子元素将如何定位,以及其他元素的关系和相互作用

在正常流中的盒子要么属于块级格式化上下文,要么属于内联格式化上下文

BFC 的产生

  1. 根元素
  2. float 不为 none;
  3. position: absolute/fixed;
  4. display: inline-block/flex/inline-flex/table-cell;
  5. overflow 不为 visible;

特性

  1. 内部的 Box 会在垂直方向,一个接一个地放置。
  2. Box 垂直方向的距离由 margin 决定。属于同一个 BFC 的两个相邻 Box 的 margin 会发生重叠.每个元素的 margin box 的左边,与包含块 border box 的左边相接触(对于从左往右的格式化,否则相反)。即使存在浮动也是如此。(如果有多个浮动盒,因为是浮动盒,所以后面的有新的 BFC,会挨着前面的从左到右排列)
  3. BFC 的区域不会与 float box 重叠。(不跟外面的浮动盒重叠,会把浮动盒挤到一边)
  4. BFC 就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素。反之也如此。
  5. 计算 BFC 的高度时,浮动元素也参与计算(包裹浮动元素).

作用

  1. 阻止 margin 合并,因为 BFC 与外界隔离,自己的 margin 也就不再合并.
  2. contain float(包裹浮动元素).

边距合并

合并场景

  1. 相邻元素合并,间距为二者中较大值
  2. 父子合并
  3. 自己合并

取消合并

  1. 加 border,padding
  2. BFC

居中

水平居中

  1. 块级元素水平居中
1
2
margin-left:auto;
margin-right:auto;
  1. 内联元素水平居中,给它们的父元素加上
1
text-align:center;

若不是内联元素想让它居中,可加display:inline-block,加了之后一般还要加下面这句,不然可能会有 bug(下面可能会空出一行)

1
vertical-align: top;
  1. 让导航栏横过来,并在同一行里均匀分布

给 ul 加 css

1
2
3
4
ul{
display:flex;
justyfy-content:space-between;
}

去掉 li 的float:left

去掉 ul 的clearfix

垂直居中

  1. 若父元素没有写 height,则直接在父元素写
1
padding: 10px 0;

子元素就可以居中,所以尽量避免父亲高度确定

以下是宽高确定(注意写宽高)

  1. 让一个元素在父级元素中绝对居中

方法一:

给父级元素加:

1
2
position: relative; //若父级元素是body可以不用加
//注意父元素需要宽高

再给自己加:

1
2
3
4
5
6
7
8
9
div{
//注意子元素也需要宽高
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
margin: auto;
}

方法二:(若不兼容 IE,工作中只要用这一种方法即可,最简单,Chrome,移动端都可以用)

给父元素加:

1
2
3
display: flex;               //让它变成一个弹性盒
justify-content: center; //水平居中
align-items: center; //垂直居中
  1. table 自带居中(兼容 IE)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<html>
<style>
.parent{
border: 1px solid red;
height: 600px;
}
.child{
border: 1px solid green;
}
</style>
<body>
<table class="parent">
<tr>
<td class="child">
文字
</td>
</tr>
</table>
</body>
</html>

文字会居中

  1. 用 div 假扮 table(兼容 IE)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<html>
<style>
div.table{
display: table;
border: 1px solid red;
height: 600px;
}

div.tr{
display: table-row;
border: 1px solid green;
}

div.td{
display: table-cell;
border: 1px solid blue;
vertical-align: middle;
}

</style>
<body>
<div class="table">
<div class="tr">
<div class="td">
文字
</div>
</div>
</div>
</body>
</html>
  1. 用 100%高度的 before 和 after
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
.parent{
border: 3px solid red;
height: 600px;
text-align: center;
}

.child{
border: 3px solid black;
display: inline-block;
width: 300px;
vertical-align: middle;
}

.parent:before{
content:'';
display: inline-block;
height: 100%;
vertical-align: middle;
}
.parent:after{
content:'';
display: inline-block;
height: 100%;
vertical-align: middle;
}
  1. 绝对定位加上 margin-top: -自身 height 的 50%
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<html>
<style>
.parent{
height: 600px;
border: 1px solid red;
position: relative;
}
.child{
border: 1px solid green;
width: 300px;
position: absolute;
top: 50%;
left: 50%;
margin-left: -150px;
height: 20px;
margin-top: -10px;
text-align: center;
}
</style>
<body>
<div class="parent">
<div class="child">
文字
</div>
</div>
</body>
</html>
  1. 使用transform

子元素加{top:50%,transform:translate(-50%)}

CSS3 有哪些新特性?

新增各种 CSS 选择器 (: not(.input):所有 class 不是“input”的节点)

圆角    (border-radius:8px)

多列布局    (multi-column layout)

阴影和反射 (box-shadow\reflect)

文字特效 (text-shadow)

文字渲染 (Text-decoration)

线性渐变 (gradient)

旋转 transform:rotate(90deg)

缩放      transform:scale(0.85,0.90)

位移      transform:translate(0px,-30px)

倾斜      transform:skew(-9deg,0deg)

动画      Animation

CSS 动画

transition(过渡)

transition的作用在于指定状态变化所需要的时间.

1
2
3
div{
transition: 1s;
}

指定属性

可以指定transition使用的属性,比如只适用于 height

transition: 1s height;

delay(延迟)

transition: 1s height,1s 1s width;

width 在一秒之后在开始变化,也就是延迟一秒.delay 可以指定动画发生的顺序,使得不同 transition 可以连在一起,形成不同效果.

transition-timing-function(缓动函数)

transition 的状态变化速度,默认不是匀速的,而是逐渐放慢的,这叫做 ease.

transition: 1s ease;

除了 ease,其他模式还有:

  1. linear: 匀速
  2. ease-in: 加速
  3. ease-out: 减速
  4. cubic-bezier: 自定义速度模式(可以在 cubic-bezier.com 里设置)

语法

简写:

transition: 1s 1s height ease;

完整写法:

1
2
3
4
transition-property: height;
transition-duration: 1s;
transition-delay: 1s;
transition-timing-function: ease;

注意

  1. transition 需要明确知道,开始状态和结束状态的具体数值,才能计算出中间状态,什么 none 到 block 之类的是不行的
  2. transition 是一次性的,不能重复发生,除非一再触发

animation(动画)

使用 animation 首先需要定义动画过程,即关键帧

1
2
3
4
5
@keyframes rainbow {
0% { background: #c00; }
50% { background: orange; }
100% { background: yellowgreen; }
}

写动画帧里的 css 记得加;号.

定义关键帧后可以给 DOM 元素绑定动画,和事件比较像.加infinite代表无限次,改成数字就是循环多少次.

1
2
3
div:hover {
animation: 1s rainbow infinite;
}

animation-fill-mode

动画结束后,会立即跳到结束状态,如果想让动画保持在结束状态,需要使用 animation-fill-mode 属性.

1
2
3
div:hover{
animation: 1s rainbow forwards;
}

animation-fill-mode 的属性:

  1. none: 默认值,回到动画未开始的状态.
  2. forwards: 让动画停留在结束状态
  3. backwards: 让动画回到第一帧的状态
  4. both: animation-direction 轮流应用 forwards 和 backwards 规则

animation-direction

动画循环播放时,每次都是从结束状态跳回到起始状态,再开始播放。animation-direction 属性,可以改变顺序

默认情况是,animation-direction 等于 normal.此外,还可以等于取 alternate(先从前往后再从后往前)、reverse(从后往前)、alternate-reverse 等值.

1
2
3
div:hover {
animation: 1s rainbow 3 normal;
}

animation-play-state

有时,动画播放过程中,会突然停止。这时,默认行为是跳回到动画的开始状态,如果想让动画保持突然终止时的状态,就要使用 animation-play-state 属性。

1
2
3
4
5
6
7
8
div {
animation: spin 1s linear infinite;
animation-play-state: paused;
}

div:hover {
animation-play-state: running;
}

语法

简写:

1
2
3
div:hover {
animation: 1s 1s rainbow linear 3 forwards normal;
}

完整:

1
2
3
4
5
6
7
8
9
div:hover {
animation-name: rainbow;
animation-duration: 1s;
animation-timing-function: linear;
animation-delay: 1s;
animation-fill-mode:forwards;
animation-direction: normal;
animation-iteration-count: 3;
}

0%可以用 from 代表,100%可以用 to 代表,因此上面的代码等同于下面的形式

1
2
3
4
5
@keyframes rainbow {
from { background: #c00 }
50% { background: orange }
to { background: yellowgreen }
}

steps(分步过渡)

浏览器从一个状态向另一个状态过渡,是平滑过渡。steps 函数可以实现分步过渡。

1
2
3
div:hover {
animation: 1s rainbow infinite steps(10);
}

歌词效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<style>
@keyframes moving {
from {
width: 0;
}
}
.line {
position: relative;
}
p {
width: 600px;
font-size: 40px;
white-space: nowrap;
overflow: hidden;
position: absolute;
top: 0;
z-index: 2;
}

p:nth-child(1) {
color: red;
animation: moving 20s
}
p:nth-child(2) {
z-index: -1;
color: #666;
}
</style>
<body>
<div class="line">
<p>To my days change my ways为生活我试着不断改变</p>
<p>This sudden end to my days这生命的终点突如其来</p>
</div>
</body>

打字机效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<style>
@keyframes typing {from { width: 0 } }
@keyframes blink-caret { 50% { border-clolr: transparent } }
h1 {
border-right: 0.1em solid;
color: #fff;
width: 352px;
height: 30px;
white-space: nowrap;
overflow: hidden;
animation: typing 5s steps(11, end),//变11次变成最终状态
blink-caret .5s infinite alternate;
}
body {
background: #000;
height: 100vh;
}
</style>

<body>
<h1>躺在你的学校操场看星空</h1>
</body>

如果需要手动写动画,你认为最小时间间隔是多久,为什么

多数显示器默认频率是 60Hz,即 1 秒刷新 60 次,所以理论上最小间隔为 1/60*1000ms = 16.7ms

transform(变形)

transform 属性主要包括 rotate(旋转),skew(扭曲),scale(缩放),translate(移动),matrix(矩阵变形)

transform : none |  [  ]*

transform 中使用多个属性时却需要有空格隔开,可用于内联(inline)元素和块级(block)元素

rotate(旋转)

可以通过 rotate 使元素旋转一定的度数

transform: rotate(30deg)//顺时针旋转30度

  1. 旋转之后元素仍占据原来位置,实际上所有的 transform 都是这样,缩放、位移等都不会改变元素占据的位置
  2. 元素旋转的的基点默认是中心,可以通过 transform-origin 属性改变

transform:rotate(30deg); transform-origin: 0% 0%;

transform-origin 的取值可以是:

  1. top, bottom, left, right, center
  2. 百分数

translate(位移)

transform: translateY(100px)//Y轴位移100px;

scale(缩放)

scale(x,y): 使元素水平方向和垂直方向同时缩放

transform: translate(3)//相同的比例缩放两个方向

skew(扭曲)

通过 skew 使元素扭曲一定的度数

transform:skew(10deg, 20deg);//x轴扭曲10度,y轴扭曲20度

3D

元素需要设置需要设置 perspective 来激活 3D 效果

  1. transform: perspective( 600px );
  2. perspective: 600px;

perspective 属性的值决定了 3D 效果的强烈程度,可以认为是观察者到页面的距离。

越大距离越远,视觉上的 3D 效果就会相应的减弱。

3D 变形方法

rotateX( angle )

rotateY( angle )

rotateZ( angle )

translateZ( tz )

scaleZ( sz )

translateX()方法使元素延 X 轴移动,translateZ()使元素延 Z 轴(在 3D 空间中方向从前到后)移动。正值使元素离观察者更近,负值使元素变远。

渐进增强和优雅降级

渐进增强(Progressive Enhancement):一开始就针对低版本浏览器进行构建页面,完成基本的功能,然后再针对高级浏览器进行效果、交互、追加功能达到更好的体验。

优雅降级(Graceful Degradation):一开始就构建站点的完整功能,然后针对浏览器测试和修复。比如一开始使用 CSS3 的特性构建了一个应用,然后逐步针对各大浏览器进行 hack 使其可以在低版本浏览器上正常浏览。

其实渐进增强和优雅降级并非什么新概念,只是旧的概念换了一个新的说法。在传统软件开发中,经常会提到向上兼容和向下兼容的概念。渐进增强相当于向上兼容,而优雅降级相当于向下兼容。

二者区别

优雅降级观点认为:

应该针对那些最高级、最完善的浏览器来设计网站。而将那些被认为“过时”或有功能缺失的浏览器下的测试工作安排在开发周期的最后阶段,并把测试对象限定为主流浏览器(如 IE、Mozilla 等)的前一个版本。在这种设计范例下,旧版的浏览器被认为仅能提供“简陋却无妨 (poor, but passable)” 的浏览体验。你可以做一些小的调整来适应某个特定的浏览器。但由于它们并非我们所关注的焦点,因此除了修复较大的错误之外,其它的差异将被直接忽略。

渐进增强观点则认为:

应关注于内容本身。请注意其中的差别:我甚至连“浏览器”三个字都没提。内容是我们建立网站的诱因。有的网站展示它,有的则收集它,有的寻求,有的操作,还有的网站甚至会包含以上的种种,但相同点是它们全都涉及到内容。这使得渐进增强成为一种更为合理的设计范例。这也是它立即被 Yahoo! 所采纳并用以构建其“分级式浏览器支持 (Graded Browser Support)”策略的原因所在。

案例分析

1
2
3
4
5
6
7
8
9
10
11
12
.transition { /*渐进增强写法*/
-webkit-transition: all .5s;
-moz-transition: all .5s;
-o-transition: all .5s;
transition: all .5s;
}
.transition { /*优雅降级写法*/
transition: all .5s;
-o-transition: all .5s;
-moz-transition: all .5s;
-webkit-transition: all .5s;
}

前缀 CSS3(-webkit-* / -moz-* / -o-*)和正常 CSS3 在浏览器中的支持情况是这样的:

  1. 很久以前:浏览器前缀 CSS3 和正常 CSS3 都不支持;
  2. 不久之前:浏览器只支持前缀 CSS3,不支持正常 CSS3;
  3. 现在:浏览器既支持前缀 CSS3,又支持正常 CSS3;
  4. 未来:浏览器不支持前缀 CSS3,仅支持正常 CSS3.

渐进增强的写法,优先考虑老版本浏览器的可用性,最后才考虑新版本的可用性。在时期 3 前缀 CSS3 和正常 CSS3 都可用的情况下,正常 CSS3 会覆盖前缀 CSS3。优雅降级的写法,优先考虑新版本浏览器的可用性,最后才考虑老版本的可用性。在时期 3 前缀 CSS3 和正常 CSS3 都可用的情况下,前缀 CSS3 会覆盖正常的 CSS3。

重绘和回流

前提

  1. 浏览器使用流式布局模型 (Flow Based Layout)。
  2. 浏览器会把 HTML 解析成 DOM,把 CSS 解析成 CSSOM,DOM 和 CSSOM 合并就产生了 Render Tree。
  3. 有了 RenderTree,我们就知道了所有节点的样式,然后计算他们在页面上的大小和位置,最后把节点绘制到页面上。
  4. 由于浏览器使用流式布局,对 Render Tree 的计算通常只需要遍历一次就可以完成,但 table 及其内部元素除外,他们可能需要多次计算,通常要花 3 倍于同等元素的时间,这也是为什么要避免使用 table 布局的原因之一。

回流必将引起重绘,重绘不一定会引起回流。

重绘

定义: 当页面中元素样式的改变并不影响它在文档流中的位置时(例如:color、background-color、visibility 等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘。

回流

定义: 当 Render Tree 中部分或全部元素的尺寸、结构、或某些属性发生改变时,浏览器重新渲染部分或全部文档的过程称为回流。

导致回流的操作

页面首次渲染

浏览器窗口大小发生改变

元素尺寸或位置发生改变

元素内容变化(文字数量或图片大小等等)

元素字体大小变化

添加或者删除可见的 DOM 元素

激活 CSS 伪类(例如::hover)

查询某些属性或调用某些方法

导致回流的属性

clientWidth、clientHeight、clientTop、clientLeft

offsetWidth、offsetHeight、offsetTop、offsetLeft

scrollWidth、scrollHeight、scrollTop、scrollLeft

scrollIntoView()、scrollIntoViewIfNeeded()

getComputedStyle()

getBoundingClientRect()

scrollTo()

性能影响

回流比重绘的代价要更高。

有时即使仅仅回流一个单一的元素,它的父元素以及任何跟随它的元素也会产生回流。

现代浏览器会对频繁的回流或重绘操作进行优化:

浏览器会维护一个队列,把所有引起回流和重绘的操作放入队列中,如果队列中的任务数量或者时间间隔达到一个阈值的,浏览器就会将队列清空,进行一次批处理,这样可以把多次回流和重绘变成一次。

当你访问以下属性或方法时,浏览器会立刻清空队列:

1
2
3
4
5
6
clientWidth、clientHeight、clientTop、clientLeft
offsetWidth、offsetHeight、offsetTop、offsetLeft
scrollWidth、scrollHeight、scrollTop、scrollLeft
width、height
getComputedStyle()
getBoundingClientRect()

因为队列中可能会有影响到这些属性或方法返回值的操作,即使你希望获取的信息与队列中操作引发的改变无关,浏览器也会强行清空队列,确保你拿到的值是最精确的。

如何避免

CSS

避免使用 table 布局。

尽可能在 DOM 树的最末端改变 class。

避免设置多层内联样式。

将动画效果应用到 position 属性为 absolute 或 fixed 的元素上。

避免使用 CSS 表达式(例如:calc())。

JavaScript

避免频繁操作样式,最好一次性重写 style 属性,或者将样式列表定义为 class 并一次性更改 class 属性。

避免频繁操作 DOM,创建一个 documentFragment,在它上面应用所有 DOM 操作,最后再把它添加到文档中。

也可以先为元素设置display: none,操作结束后再把它显示出来。因为在 display 属性为 none 的元素上进行的 DOM 操作不会引发回流和重绘。

避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。

对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。

浏览器兼容性问题

样式兼容性(css)方面

  1. 因为历史原因,不同的浏览器样式存在差异,可以通过 Normalize.css 抹平差异,也可以定制自己的 reset.css,例如通过通配符选择器,全局重置样式.
1
2
3
* {
margin: 0; padding: 0;
}
  1. 在 CSS3 还没有成为真正的标准时,浏览器厂商就开始支持这些属性的使用了。CSS3 样式语法还存在波动时,浏览器厂商提供了针对浏览器的前缀,直到现在还是有部分的属性需要加上浏览器前缀。在开发过程中我们一般通过 IDE 开发插件、css 预处理器以及前端自动化构建工程帮我们处理。如 css-loader.
  2. 在还原设计稿的时候我们常常会需要用到透明属性,所以解决 IE9 以下浏览器不能使用 opacit。
1
2
3
opacity: 0.5;
filter: alpha((opacity = 50)); //IE6-IE8我们习惯使用filter滤镜属性来进行实现
filter: progid: DXImageTransform.Microsoft.Alpha((style = 0), (opacity = 50)); //IE4-IE9都支持滤镜写法progid:DXImageTransform.Microsoft.Alpha(Opacity=xx)

浏览器 hack

1.快速判断 IE 浏览器版本

1
2
3
<!--[if IE 8]> ie8 <![endif]-->

<!--[if IE 9]> ie9 浏览器 <![endif]-->

2.判断是否是 Safari 浏览器

1
2
/* Safari */
var isSafari = /a/.__proto__ == "//";

3.判断是否是 Chrome 浏览器

1
2
/* Chrome */
var isChrome = Boolean(window.chrome);

js 兼容

1.事件兼容的问题,我们通常需要会封装一个适配器的方法,过滤事件句柄绑定、移除、冒泡阻止以及默认事件行为处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
var helper = {};

//绑定事件
helper.on = function (target, type, handler) {
if (target.addEventListener) {
target.addEventListener(type, handler, false);
} else {
target.attachEvent(
"on" + type,
function (event) {
return handler.call(target, event);
},
false
);
}
};

//取消事件监听
helper.remove = function (target, type, handler) {
if (target.removeEventListener) {
target.removeEventListener(type, handler);
} else {
target.detachEvent(
"on" + type,
function (event) {
return handler.call(target, event);
},
true
);
}
};

2.new Date()构造函数使用,’2018-07-05’是无法被各个浏览器中,使用 new Date(str)来正确生成日期对象的。 正确的用法是’2018/07/05’.

3.获取 scrollTop 通过 document.documentElement.scrollTop 兼容非 chrome 浏览器

1
var scrollTop = document.documentElement.scrollTop || document.body.scrollTop;


title: cNode 社区项目 date: 2019-11-27 15:57:45

tags: 项目

实战项目: 高仿 CNODE 社区

使用 Vue-cli2 搭建,包含 webpack 配置.

项目模块组件:

Header 模块

PostList 模块

Article 模块

Slider 侧边栏模块

UserInfo 用户个人中心模块

Pagination 分页组件的开发

主要用到的技术栈有:

vue.js 计算属性

vue.js 的内置指令和事件的绑定

vue.js 的自定义事件和触发

vue-router 路由的跳转和监听

父子组件之间的数据传递