├── .gitignore
├── .gitmodules
├── .npmrc
├── .prettierrc.js
├── .travis.yml
├── README.md
├── _config.yml
├── package.json
├── roadmaps.json
├── scaffolds
├── draft.md
├── page.md
└── post.md
├── script
├── buildRoadmaps.js
├── buildTutorials.js
├── deploy.js
├── downloadRepos.js
├── movePublicToRoot.js
└── utils
│ ├── buildRoadmap.js
│ ├── buildTutorial.js
│ └── index.js
├── source
├── 404
│ └── index.md
├── FAQ
│ └── index.md
├── _data
│ ├── iconfont.eot
│ ├── iconfont.json
│ ├── iconfont.svg
│ ├── iconfont.ttf
│ ├── iconfont.woff
│ ├── iconfont.woff2
│ └── styles.styl
├── _posts
│ ├── @10aaf06.md
│ ├── @2629c44.md
│ ├── @34dvBzFh6.md
│ ├── @4675l54tY.md
│ ├── @5e3e0d2.md
│ ├── @5e60587.md
│ ├── @70fc9b9.md
│ ├── @auY0siFek.md
│ ├── @d5269af.md
│ ├── @e215d5a.md
│ ├── @pRtgJQ4NP.md
│ ├── @sO4iOISav.md
│ ├── @sQN91Mviv.md
│ └── @uXOOfFmhS.md
├── about
│ └── index.md
├── categories
│ └── index.md
├── images
│ ├── avatars
│ │ ├── bldtp.png
│ │ ├── cebuzhun.png
│ │ ├── crxk.jpg
│ │ ├── mRcfps.jpg
│ │ ├── manyipai.png
│ │ ├── pftom.jpg
│ │ ├── tuture-dev.jpg
│ │ ├── zw.png
│ │ └── zwmxs.png
│ ├── logos
│ │ ├── Angular.svg
│ │ ├── Go.svg
│ │ ├── Java.svg
│ │ ├── Nodejs.svg
│ │ ├── Python.svg
│ │ ├── React.svg
│ │ └── Vue.svg
│ └── social
│ │ └── wechat.png
├── roadmaps
│ └── index.md
├── schedule
│ └── index.md
├── sitemap
│ └── index.md
└── tags
│ └── index.md
├── tutorials.json
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | Thumbs.db
3 | db.json
4 | *.log
5 | node_modules/
6 | public/
7 | .deploy*/
8 | .vscode
9 | .env
10 |
11 | source/_posts/*
12 | !source/_posts/@*.md
13 | source/images/covers
14 | source/roadmaps/*
15 | !source/roadmaps/index.md
16 |
17 | repos
18 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "themes/next"]
2 | path = themes/next
3 | url = https://github.com/tuture-dev/hexo-next-theme.git
4 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock=false
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | bracketSpacing: true,
3 | singleQuote: true,
4 | jsxBracketSameLine: true,
5 | trailingComma: 'all',
6 | printWidth: 80,
7 | };
8 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | notifications:
2 | webhooks:
3 | urls:
4 | - https://open.feishu.cn/officialapp/notify/6ce8fd9560a63f21cd1b28abdec2fe5573fb9bdab3625ca879920e6b93f3f7a8
5 | on_success: always # default: always
6 | on_failure: always # default: always
7 | on_start: never # default: never
8 | on_cancel: always # default: always
9 | on_error: always # default: always
10 |
11 | language: node_js
12 |
13 | dist: xenial
14 |
15 | node_js:
16 | - 12
17 |
18 | env:
19 | - ID_DIGITS=7
20 |
21 | before_script:
22 | - npm i -g @tuture/cli
23 |
24 | script:
25 | - yarn
26 | - yarn download
27 | - yarn build:roadmaps
28 | - yarn build:tutorials
29 | - yarn clean
30 | - yarn algolia
31 | - yarn build
32 | - node script/deploy.js
33 |
34 | cache: yarn
35 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 图雀社区主站
2 |
3 | 本项目是一个 Hexo 博客,这里汇集了由社区贡献的、通过 [Tuture](https://github.com/tuture-dev/tuture) 工具写成的优质实战教程。
4 |
5 | > **🇨🇳目前仅支持中文教程!Currently only Chinese tutorials are supported!**
6 |
7 | ## 本地查看
8 |
9 | 首先确保本地已安装 tuture,如果没有则通过 `npm install -g tuture` 安装。然后将仓库克隆到本地(包括所有 Git 子模块):
10 |
11 | ```bash
12 | $ git clone --recurse-submodules https://github.com/tuture-dev/hub.git
13 | ```
14 |
15 | 进入仓库,安装 npm 依赖:
16 |
17 | ```bash
18 | cd hub
19 | npm install
20 | ```
21 |
22 | 下载所有学习路线和教程,并构建(需要几分钟的时间):
23 |
24 | ```bash
25 | $ npm run download
26 | $ npm run build:roadmaps
27 | $ npm run build:tutorials
28 | ```
29 |
30 | 最后打开 hexo 服务器:
31 |
32 | ```bash
33 | $ npm start
34 | ```
35 |
36 | 然后访问 `localhost:5000` 即可在本地查看图雀社区主站啦!(⚠️注意:搜索功能无法使用)
37 |
38 | ## 常见问题(FAQs)
39 |
40 | 我们对常见的问题都进行了解答,请访问[图雀社区 FAQ](https://tuture.co/FAQ/)。
41 |
42 | ## 贡献教程
43 |
44 | 首先,非常感谢你选择分享教程!分享教程非常容易,请阅读[分享教程指南](https://docs.tuture.co/guide/sharing.html)。
45 |
46 | ## 关注我们
47 |
48 | 想要第一时间获取最新教程的通知?不妨关注我们的微信公众号吧:
49 |
50 | 
51 |
--------------------------------------------------------------------------------
/_config.yml:
--------------------------------------------------------------------------------
1 | # Hexo Configuration
2 | ## Docs: https://hexo.io/docs/configuration.html
3 | ## Source: https://github.com/hexojs/hexo/
4 |
5 | # Site
6 | title: 图雀社区
7 | subtitle: 汇集精彩的实战技术教程
8 | description: 图雀社区是一个供大家分享用 Tuture 写作工具完成的教程的一个平台。在这里,读者们可以尽情享受高质量且免费的实战教程,并能与作者和其他读者互动和讨论;而作者们也可以借此传播他们的技术知识,宣传他们的开源项目,找到自己输出内容的受众,加速技术的传播。
9 | keywords: 图雀社区,Tuture,Vue.js实战教程,微信小程序,Kotlin,React Native,Webpack,MVVM,React.js,Node.js,Redux,Django,MongoDB,Docker,JavaScript,Java,Go,Kubernetes,Nuxt,vue-router,react-router,小程序,跨端开发,Taro,react hooks,redux-saga,learn by doing,Web 前端实战教程,后端实战教程,小程序实战教程,移动端实战教程
10 | author: 图雀社区
11 | language: zh-CN
12 | timezone:
13 |
14 | # URL
15 | ## If your site is put in a subdirectory, set url as 'http://yoursite.com/child' and root as '/child/'
16 | url: https://tuture.co/
17 | root: /
18 | permalink: :year/:month/:day/:title/
19 | permalink_defaults:
20 |
21 | # Directory
22 | source_dir: source
23 | public_dir: public
24 | tag_dir: tags
25 | archive_dir: archives
26 | category_dir: categories
27 | code_dir: downloads/code
28 | i18n_dir: :lang
29 | skip_render:
30 |
31 | # Writing
32 | new_post_name: :title.md # File name of new posts
33 | default_layout: post
34 | titlecase: false # Transform title into titlecase
35 | external_link: true # Open external links in new tab
36 | filename_case: 0
37 | render_drafts: false
38 | post_asset_folder: true
39 | relative_link: false
40 | future: true
41 | highlight:
42 | enable: true
43 | line_number: false
44 | auto_detect: false
45 | tab_replace:
46 |
47 | # Home page setting
48 | # path: Root path for your blogs index page. (default = '')
49 | # per_page: Posts displayed per page. (0 = disable pagination)
50 | # order_by: Posts order. (Order by date descending by default)
51 | index_generator:
52 | path: ""
53 | per_page: 10
54 | order_by: -date
55 |
56 | # Category & Tag
57 | default_category: uncategorized
58 | category_map:
59 | tag_map:
60 |
61 | # Date / Time format
62 | ## Hexo uses Moment.js to parse and display date
63 | ## You can customize the date format as defined in
64 | ## http://momentjs.com/docs/#/displaying/format/
65 | date_format: YYYY-MM-DD
66 | time_format: HH:mm:ss
67 |
68 | # Pagination
69 | ## Set per_page to 0 to disable pagination
70 | per_page: 10
71 | pagination_dir: page
72 |
73 | # Extensions
74 | ## Plugins: https://hexo.io/plugins/
75 | ## Themes: https://hexo.io/themes/
76 | theme: next
77 |
78 | # Deployment
79 | ## Docs: https://hexo.io/docs/deployment.html
80 | deploy:
81 | - type: git
82 | repo: git@github.com:tutureproject/tutureproject.github.io.git
83 | branch: master
84 | - type: git
85 | repo: git@github.com:tutureproject/tutureproject.github.io.git
86 | branch: src
87 | extend_dirs: /
88 | ignore_hidden: false
89 | ignore_pattern:
90 | public: .
91 |
92 | algolia:
93 | applicationID: 73BSUE4RKU
94 | apiKey: 0b7cce26a4734cb760080065b3c4a4a1
95 | indexName: Tuture
96 | chunkSize: 5000
97 |
98 | # Post wordcount display settings
99 | # Dependencies: https://github.com/theme-next/hexo-symbols-count-time
100 | symbols_count_time:
101 | symbols: true
102 | time: true
103 | total_symbols: true
104 | total_time: true
105 |
106 | baidusitemap:
107 | path: baidusitemap.xml
108 |
109 | nofollow:
110 | enable: true
111 | exclude:
112 | - https://docs.tuture.co/
113 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tuture-hub",
3 | "version": "0.0.0",
4 | "private": true,
5 | "hexo": {
6 | "version": "4.2.0"
7 | },
8 | "scripts": {
9 | "start": "hexo server",
10 | "clean": "hexo clean",
11 | "download": "node script/downloadRepos.js",
12 | "build:roadmaps": "node script/buildRoadmaps.js",
13 | "build:tutorials": "node script/buildTutorials.js",
14 | "build": "hexo generate",
15 | "algolia": "hexo algolia"
16 | },
17 | "dependencies": {
18 | "ali-oss": "^6.5.1",
19 | "chalk": "^3.0.0",
20 | "fs-extra": "^8.1.0",
21 | "hexo": "^4.1.0",
22 | "hexo-algolia": "^1.3.1",
23 | "hexo-autonofollow": "^1.0.1",
24 | "hexo-deployer-git": "^2.0.0",
25 | "hexo-generator-archive": "^0.1.5",
26 | "hexo-generator-baidu-sitemap": "^0.1.6",
27 | "hexo-generator-category": "^0.1.3",
28 | "hexo-generator-feed": "^2.0.0",
29 | "hexo-generator-index": "^0.2.1",
30 | "hexo-generator-sitemap": "^1.2.0",
31 | "hexo-generator-tag": "^0.2.0",
32 | "hexo-renderer-ejs": "^0.3.1",
33 | "hexo-renderer-marked": "^1.0.1",
34 | "hexo-renderer-stylus": "^0.3.3",
35 | "hexo-server": "^0.3.3",
36 | "hexo-symbols-count-time": "^0.6.1",
37 | "hexo-util": "^1.7.0",
38 | "js-yaml": "^3.13.1",
39 | "p-limit": "^2.2.2",
40 | "p-retry": "^4.2.0"
41 | }
42 | }
--------------------------------------------------------------------------------
/roadmaps.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": "repos/roadmaps",
3 | "sources": [
4 | {
5 | "name": "node",
6 | "git": "https://github.com/tuture-dev/nodejs-roadmap.git"
7 | },
8 | {
9 | "name": "react",
10 | "git": "https://github.com/tuture-dev/react-roadmap.git"
11 | }
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/scaffolds/draft.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: {{ title }}
3 | tags:
4 | ---
5 |
--------------------------------------------------------------------------------
/scaffolds/page.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: {{ title }}
3 | date: {{ date }}
4 | ---
5 |
--------------------------------------------------------------------------------
/scaffolds/post.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: { { title } }
3 | date: { { date } }
4 | tags:
5 | keywords:
6 | description:
7 | ---
8 |
--------------------------------------------------------------------------------
/script/buildRoadmaps.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const { listSubdirectories, buildRoadmap } = require('./utils');
3 |
4 | const root = process.cwd();
5 | const roadmapsRoot = path.resolve('repos', 'roadmaps');
6 |
7 | console.log('Building roadmaps ...');
8 | listSubdirectories(roadmapsRoot).forEach(p => buildRoadmap(p));
9 |
10 | process.chdir(root);
11 |
--------------------------------------------------------------------------------
/script/buildTutorials.js:
--------------------------------------------------------------------------------
1 | const cp = require('child_process');
2 | const path = require('path');
3 | const { listSubdirectories, buildTutorial } = require('./utils');
4 |
5 | const root = process.cwd();
6 | const tutorialsRoot = path.resolve('repos', 'tutorials');
7 |
8 | console.log('Tuture Version');
9 | console.log(cp.execSync('tuture -v').toString());
10 |
11 | console.log('\nBuilding tutorials ...');
12 | listSubdirectories(tutorialsRoot).forEach(p => buildTutorial(p));
13 |
14 | process.chdir(root);
15 |
--------------------------------------------------------------------------------
/script/deploy.js:
--------------------------------------------------------------------------------
1 | const chalk = require('chalk');
2 | const fs = require('fs');
3 | const path = require('path');
4 | const retry = require('retry');
5 | const pLimit = require('p-limit');
6 | const OSS = require('ali-oss');
7 |
8 | const client = new OSS({
9 | accessKeyId: process.env.ACCESS_KEY_ID,
10 | accessKeySecret: process.env.ACCESS_KEY_SECRET,
11 | bucket: process.env.BUCKET_NAME,
12 | region: process.env.BUCKET_REGION,
13 | });
14 |
15 | const distDir = 'public';
16 | const filePaths = [];
17 |
18 | function walk(dirName) {
19 | const files = fs.readdirSync(dirName);
20 |
21 | files.forEach(file => {
22 | const fullPath = path.join(dirName, file);
23 | const stat = fs.statSync(fullPath);
24 |
25 | if (stat.isDirectory()) {
26 | walk(fullPath);
27 | } else {
28 | filePaths.push(fullPath);
29 | }
30 | });
31 | }
32 |
33 | walk(distDir);
34 |
35 | const limit = pLimit(2);
36 |
37 | const uploadTasks = filePaths.map(filePath =>
38 | limit(async () => {
39 | await client.put(filePath.substr(distDir.length + 1), filePath);
40 | console.log(`Upload ${filePath} successfully.`);
41 | }),
42 | );
43 |
44 | (async () => {
45 | await Promise.all(uploadTasks);
46 | console.log('Upload complete!');
47 | })();
48 |
--------------------------------------------------------------------------------
/script/downloadRepos.js:
--------------------------------------------------------------------------------
1 | const chalk = require('chalk');
2 | const path = require('path');
3 | const fs = require('fs-extra');
4 | const { exec } = require('child_process');
5 |
6 | const roadmaps = require('../roadmaps.json');
7 | const tutorials = require('../tutorials.json');
8 |
9 | function log(status, message) {
10 | let output;
11 | switch (status) {
12 | case 'info':
13 | output = `${chalk.blue('[INFO]')} ${message}`;
14 | break;
15 | case 'warning':
16 | output = `${chalk.yellow('[WARNING]')} ${message}`;
17 | break;
18 | case 'success':
19 | output = `${chalk.green('[SUCCESS]')} ${message}`;
20 | break;
21 | case 'fail':
22 | output = `${chalk.red('[FAIL]')} ${message}`;
23 | break;
24 | default:
25 | throw new Error(`Unsupported status: ${status}`);
26 | }
27 |
28 | console.log(output);
29 | }
30 |
31 | function downloadAll(object) {
32 | const { root, sources } = object;
33 | log('info', `Downloading ${root} with ${sources.length} sources ...`);
34 |
35 | const tasks = sources.map(
36 | source =>
37 | new Promise((resolve, reject) => {
38 | const repoPath = path.join(root, source.name);
39 | log('info', `Starting to download ${repoPath} ...`);
40 |
41 | // 如果已经下载过,则删除重新下载
42 | if (fs.existsSync(repoPath)) {
43 | log('warning', `Deleting ${repoPath} and re-download.`);
44 | fs.removeSync(repoPath);
45 | }
46 |
47 | exec(`git clone ${source.git} ${repoPath}`, err => {
48 | if (err) {
49 | log('fail', `Download failed: ${repoPath}!`);
50 | reject(err.message);
51 | } else {
52 | log('success', `Finished ${repoPath}!`);
53 | resolve();
54 | }
55 | });
56 | }),
57 | );
58 |
59 | Promise.all(tasks)
60 | .then(() => {
61 | log('success', `Download ${root} complete!`);
62 | })
63 | .catch(err => {
64 | log('fail', `Download ${root} failed: ${err}`);
65 | });
66 | }
67 |
68 | downloadAll(roadmaps);
69 | downloadAll(tutorials);
70 |
--------------------------------------------------------------------------------
/script/movePublicToRoot.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs-extra');
2 | const path = require('path');
3 |
4 | const buildDir = 'public';
5 | const excludedFiles = ['.git', '.gitignore', 'node_modules', buildDir];
6 |
7 | if (!fs.existsSync(buildDir)) {
8 | console.log('Build directory not ready. Stopping.');
9 | process.exit(1);
10 | }
11 |
12 | fs.readdirSync('.')
13 | .filter(fname => excludedFiles.indexOf(fname) < 0)
14 | .forEach(fname => fs.removeSync(fname));
15 |
16 | fs.readdirSync(buildDir).forEach(fname =>
17 | fs.moveSync(path.join(buildDir, fname), fname, { overwrite: true }),
18 | );
19 |
--------------------------------------------------------------------------------
/script/utils/buildRoadmap.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs-extra');
2 | const path = require('path');
3 |
4 | // Root path of this project.
5 | const root = process.cwd();
6 |
7 | // Path to hexo posts.
8 | const roadmapsDir = path.join(root, 'source', 'roadmaps');
9 |
10 | function buildRoadmap(roadmapPath) {
11 | process.chdir(roadmapPath);
12 | console.log(`Working on ${process.cwd()}.`);
13 |
14 | if (!fs.existsSync('README.md')) {
15 | console.log('Not a valid roadmap, skipping.');
16 | process.chdir(root);
17 | return;
18 | }
19 |
20 | const roadmapName = path.parse(roadmapPath).name;
21 | let content = fs.readFileSync('README.md').toString();
22 | const frontmatter = fs.readFileSync('frontmatter.yml').toString();
23 |
24 | // Remove markdown TOC.
25 | content = content.replace(/## 目录[\w\W]+## 入门/, '## 入门');
26 |
27 | // Remove original h1.
28 | content = content.replace(/
[\w\W]+?<\/h1>/, '');
29 |
30 | // Append front matter.
31 | content = `${frontmatter}\n${content}`;
32 |
33 | // Save roadmap to target directory.
34 | const targetDir = path.join(roadmapsDir, roadmapName);
35 | fs.ensureDirSync(targetDir);
36 | fs.writeFileSync(path.join(targetDir, 'index.md'), content);
37 |
38 | // Move assets directory.
39 | fs.copySync('assets', path.join(targetDir, 'assets'));
40 | }
41 |
42 | module.exports = buildRoadmap;
43 |
--------------------------------------------------------------------------------
/script/utils/buildTutorial.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs-extra');
2 | const path = require('path');
3 | const cp = require('child_process');
4 | const yaml = require('js-yaml');
5 |
6 | // Root path of this project.
7 | const root = process.cwd();
8 |
9 | // Path to hexo posts.
10 | const postsDir = path.join(root, 'source', '_posts');
11 |
12 | // Sub-directory for storing markdowns of each tutorial.
13 |
14 | const workspace = '.tuture';
15 |
16 | const collectionPath = path.join(workspace, 'collection.json');
17 |
18 | const buildDir = path.join(workspace, 'build');
19 |
20 | /**
21 | * Function for adjusting markdown content in place.
22 | */
23 | function adjustContent(markdownPath, info) {
24 | const { cover, id } = info;
25 |
26 | let content = fs.readFileSync(markdownPath).toString();
27 |
28 | // Set the lang of all vue code blocks to html,
29 | // since highlight.js doesn't support vue syntax.
30 | content = content.replace(/```vue/g, '```html');
31 |
32 | // Replace tsx to ts.
33 | content = content.replace(/```tsx/g, '```ts');
34 |
35 | fs.writeFileSync(markdownPath, content);
36 | }
37 |
38 | /**
39 | * Build a single hexo post.
40 | */
41 | function buildSingleArticle(article) {
42 | const { name, id, cover } = article;
43 | const truncatedId = id
44 | .toString()
45 | .slice(0, Number(process.env.ID_DIGITS) || 7);
46 | const mdPath = path.join(buildDir, `${name}.md`);
47 |
48 | adjustContent(mdPath, { cover, id: truncatedId });
49 |
50 | fs.copySync(mdPath, path.join(postsDir, `${truncatedId}.md`), {
51 | overwrite: true,
52 | });
53 | }
54 |
55 | /**
56 | * Build tutorials and move them into hexo posts directory.
57 | */
58 | function buildTutorial(tuturePath) {
59 | process.chdir(tuturePath);
60 | console.log(`\nWorking on ${process.cwd()}.`);
61 |
62 | // Build tutorial as usual.
63 | cp.execSync('tuture reload && tuture build --hexo');
64 | console.log('Build complete.');
65 |
66 | const collection = JSON.parse(fs.readFileSync(collectionPath).toString());
67 | const idDigits = Number(process.env.ID_DIGITS) || 7;
68 | const convertId = (id) => id.toString().slice(0, idDigits);
69 |
70 | collection.articles.forEach((article) => buildSingleArticle(article));
71 |
72 | console.log(`Finished ${process.cwd()}.`);
73 | process.chdir(root);
74 | }
75 |
76 | module.exports = buildTutorial;
77 |
--------------------------------------------------------------------------------
/script/utils/index.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs-extra');
2 | const path = require('path');
3 |
4 | function listSubdirectories(root) {
5 | return fs
6 | .readdirSync(root)
7 | .map(p => path.resolve(root, p))
8 | .filter(p => fs.lstatSync(p).isDirectory());
9 | }
10 |
11 | exports.listSubdirectories = listSubdirectories;
12 |
13 | exports.buildRoadmap = require('./buildRoadmap');
14 | exports.buildTutorial = require('./buildTutorial');
15 |
--------------------------------------------------------------------------------
/source/404/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 404
3 | date: 1970-01-01 00:00:00
4 | comments: false
5 | ---
6 |
--------------------------------------------------------------------------------
/source/FAQ/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 常见问题说明
3 | date: 2019-11-23 10:38:31
4 | ---
5 |
6 | **Q:图雀社区到底是个什么样的社区?**
7 |
8 | A:简单来说,图雀社区是一个供大家分享用 [Tuture](https://github.com/tuture-dev/tuture) 写作工具完成的教程的一个平台。在这里,读者们可以尽情享受高质量的实战教程,并且与作者和其他读者互动和讨论;而作者们也可以借此传播他们的技术知识,宣传他们的开源项目。
9 |
10 |
11 | **Q:这个 Tuture 写作工具到底是个什么玩意儿?**
12 |
13 | A:Tuture 写作工具说起来很简单:把你的 Git 仓库转换成一个教程的“骨架”。接着你需要做的仅仅只是填充“血肉”——也就是给你的代码变化(Git Diff)提供讲解。听上去这个想法很普通,但是经过我们反复实践,发现这种写作方式节省了很多整理代码的时间,并且能够让写作者专注于项目的组织和概念的讲解。所有用 Tuture 工具完成的教程不仅写起来很快(即便一个大型实战也只要几天的时间),而且质量有保障(代码不会出错,读者总能够跟着敲)。如果看到这里,您迫不及待的想要尝试一下 Tuture 写作工具的话,那么您可以访问我们的[文档地址](https://www.yuque.com/tuture/product-manuals),我们撰写了一篇简洁易懂的教程教您使用它哦!
14 |
15 |
16 | **Q:现在的技术社区已经这么多了,图雀社区有什么特别之处吗?**
17 |
18 | A:正如之前所言,图雀社区是由 [Tuture](https://github.com/tuture-dev/tuture) 写作工具驱动的,这也意味着我们的特点是:**专注于讲透代码的文字教程,并且每篇教程与一个 Git 仓库完全对应,因此文中代码都是能够让你从头跟着敲到尾,最终一定能够写出实际可运行的项目**。我们认为这种“边学边做”的方式能够最大化学习效率,让你快速掌握一门技术,还能收获满满的成就感。
19 |
20 |
21 | **Q:图雀社区的内容会收费吗?**
22 |
23 | A:我们承诺,所有内容面向广大读者**永久免费**。
24 |
25 |
26 | **Q:内容永久免费还怎么吸引作者来创作?**
27 |
28 | A:知识付费很常见,但却又是**矛盾**的:作者希望自己高质量的内容能够得到更多的报酬,而读者却希望能够以最小的代价获取。免费的内容当然能够吸引读者,但是凭什么能够吸引作者?图雀社区的思路是,先通过几位发起人的努力产出高质量的实战教程,并将其发表各大平台来积累忠实读者;之后投稿的文章,图雀社区将以保留署名的方式进行发表,帮助每位作者推广他们的文章和项目,快速建立人气和知名度,而不用再经历漫长的等待。
29 |
30 |
31 | **Q:如果我是作者,想要投稿到图雀,该怎么做?**
32 |
33 | A:首先感谢你打算加入到图雀作者的大家庭!您可以参考我们的[安装文档](https://www.yuque.com/tuture/product-manuals/installation),配置好写作工具;然后浏览[《开始写作》](https://www.yuque.com/tuture/product-manuals/initialization)文档学习如何编写教程, 最后查看[《分享教程》](https://www.yuque.com/tuture/product-manuals/sharing)了解如何发布文章到图雀社区。我们希望能够帮助您的文章、项目、知识和经验收获应有的认可!
34 |
35 | **Q:如果我是个普通的读者,想要为图雀社区出一份力,该怎么做?**
36 |
37 | A:图雀酱再次表示感谢!您只需要在微信搜索 「图雀社区」关注我们的微信公众号就好啦,或者您是掘金、知乎或者简书、CSDN 等的用户,也可以在对应的平台上支持我们哦:
38 |
39 | - 图雀社区官方网站:[网站地址](http://tuture.co/)
40 | - 微信公众号:[二维码地址](https://tuture.co/images/social/wechat.png)
41 | - 掘金专栏:[网站地址](https://juejin.im/user/5b33414351882574b9694d28)
42 | - 知乎专栏:[网站地址](https://www.zhihu.com/people/tuture-dev/activities)
43 | - CSDN:[网站地址](https://tuture.blog.csdn.net/)
44 |
45 | 当然喽,如果你打算向圈内好友推荐一波图雀社区就更好啦。还有,要记得 ”常回来看看哟“~
46 |
47 |
48 | **Q:“图雀”这个名字有什么含义,图灵 + 语雀?**
49 |
50 | A:图雀可以是“图灵 + 语雀”,可以是“想要一展宏图的燕雀”,也可以是 Tuture 写作工具的音译。一千个人眼中有一千只图雀,它的含义由你决定哦。
51 |
--------------------------------------------------------------------------------
/source/_data/iconfont.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tuture-dev/hub/0a5c20a6764793c0ac4c027f0f5980179ce284b3/source/_data/iconfont.eot
--------------------------------------------------------------------------------
/source/_data/iconfont.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "",
3 | "name": "",
4 | "font_family": "iconfont",
5 | "css_prefix_text": "icon-",
6 | "description": "",
7 | "glyphs": [
8 | {
9 | "icon_id": "4565635",
10 | "name": "jianshu",
11 | "font_class": "jianshu",
12 | "unicode": "e60c",
13 | "unicode_decimal": 58892
14 | },
15 | {
16 | "icon_id": "6773372",
17 | "name": "zhihu (1)",
18 | "font_class": "zhihu",
19 | "unicode": "e688",
20 | "unicode_decimal": 59016
21 | },
22 | {
23 | "icon_id": "10633713",
24 | "name": "we-chat",
25 | "font_class": "wechat",
26 | "unicode": "e502",
27 | "unicode_decimal": 58626
28 | },
29 | {
30 | "icon_id": "11588751",
31 | "name": "csdn",
32 | "font_class": "csdn",
33 | "unicode": "e515",
34 | "unicode_decimal": 58645
35 | },
36 | {
37 | "icon_id": "12150404",
38 | "name": "juejin",
39 | "font_class": "juejin",
40 | "unicode": "e503",
41 | "unicode_decimal": 58627
42 | }
43 | ]
44 | }
45 |
--------------------------------------------------------------------------------
/source/_data/iconfont.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
42 |
--------------------------------------------------------------------------------
/source/_data/iconfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tuture-dev/hub/0a5c20a6764793c0ac4c027f0f5980179ce284b3/source/_data/iconfont.ttf
--------------------------------------------------------------------------------
/source/_data/iconfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tuture-dev/hub/0a5c20a6764793c0ac4c027f0f5980179ce284b3/source/_data/iconfont.woff
--------------------------------------------------------------------------------
/source/_data/iconfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tuture-dev/hub/0a5c20a6764793c0ac4c027f0f5980179ce284b3/source/_data/iconfont.woff2
--------------------------------------------------------------------------------
/source/_data/styles.styl:
--------------------------------------------------------------------------------
1 | @font-face {font-family: "iconfont";
2 | src: url('iconfont.eot?t=1576828227650'); /* IE9 */
3 | src: url('iconfont.eot?t=1576828227650#iefix') format('embedded-opentype'), /* IE6-IE8 */
4 | url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAAcYAAsAAAAADNwAAAbLAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHEIGVgCDQAqMXIloATYCJAMYCw4ABCAFhG0HUhuQClGUT1KG7Mdh7DwJxahUroyLl1gJC6pwEw/Q/b4/M3dubCdF4noUJS1gJK2twBJta4kX9k+wN4Tb/m9F5Kw+wqgCBKvASIQR2mPoi6rE9zv9P7cMNt38lLCzEcci5qWzk1MtmnvXQQvtSExn4pVXJyIcHDNdJA4sb21ziSyLatoAJzig7g+w6fD1H+QBce6v+gvsbAs6knCbgHrDIiDL6uJqwJFIqgAFR1azHnB8ZimFFFS6ImZnCkDmAUIVp6O3AHBu/nz4BeICB4jyDEgrt3byjCDnHXVMw8YNjQONOgJI6nOC2UeGGUAinMa6T2C2csZU18e5cYsj+I1KFN+xz24OgWPa0BDwjrryEbnrNcp/8sjkkiASCiCpGxG7Fst5R1WQ8S6O07gBj8SHgEfgY5pZJPVFjeEBIBaoPUPsoLOU4EsUskalZ6iIRwhS2GwJ118oECACMRnmAgkWS//EMVs2GST/Pvt/Pz3UXKFwvJXVYfPSxeP8fBQtwQJXC2HaPsRLMS7mCuKtBQUIC25F1Fh+ablqy5ibWsb+mePQyAPGfFYJXHAtN2/+lEDKlW4j5LVwNWBYfXkhTCjKg1wsFtP5IdRjkkvYVciYf2ktonMaic+xOFAia7ou0JZMR/ielBLNbOvoY57yE2O+oSHYcdWS3u6jM1e6rPGDRzW7fhqLT92DBGvDrTSvqvIIiDVpiSwdxsPTIZICvwnIqYYUr7gYPYtmL37WPALjitB7tTNL/iPh0/q9SfdTfqz9V/FLK+/uHIT7J7vLl8/XB14W+1n0VvVX91+x/z9Mqi/WJNosl+Tu/dNoeRFcwo6H0Jd2WFd2xpbLLbob7PkuEUU9Zt8/c/Vd8tEfSdUS6cjIijTtUkFJaAJz21nxBpxKdRy9V207JSuXditjBpWfBp0lFZqn48pVa450eJpzgZW9Y5NZICGImXuZ2QxsyRKdfHubGtuRI/P5Elbv7ADpvM/f3s3MH32CODnu3ukD2VmfeBxlNJVT5jZdud5QOTT2OQ4h/hyMEGpy95/V4OiTB87LJXg0sF8H9P6BFl7cJ/HoM8RNhd9fmQec5j1FH+Kn/OCEeEjDCOnm0p7n59zOYxumzvnLx2PDnYmPwVRY4Ifw1s3oMgZ7KTteWdcjExfIw5aNRdYo7QqpiywocNp91+AKQqQYvOe6pN6b7xxjYPe0IgJ/GZfAFvWiDWO+uifz1Swfw6T1+cygELWNeYp0JofnWbknHyXMl6VmTfEQFbMm+ExOngxG8OoavidDzh5JbECgj/Atsh4MGyd/3HAA60XJghqmFzo9eXNF3ojXdWG5XWtm7jGIc9rQiofA//9AHVZxYN5Z1CS/9W+daU1wv3tYzVY+uJNVagKltpwQRWnQQWlOkWJ0jrRjnmVuqGdp30RFCq0L0etKJYUBI4PEBcgDDnkt6Hjv757ucU5SkatW69NgTCT84ipBwYgS7il2UWiUh5XsVkoKZJWgbFWqkpxuclPIBSPKPUM93OA3p51dbkrspeTLE8LtL8l38f6qRwC+7BAev+EsUnlG3ANDLTdxkQi/KQt0oSZ3X9TJMcRHxCTI9V3zBmXixrM5kgkRXkXsHPd165feJXbeINeVTYZogfph33+kKm/EBB9KWf4LicliNn5NXBhTEnZao/3l1zav673jx1VU/gbJLfGCMVzOj5LyIJYyIqE8Blfkci5Oejdx/3pSjifSzXuvzb3/k0oRsyi57k5S/TKzn0yKViIkqL2rVbUWtZboS1SsLSXkaQ8+jf485rP750P3ZQ654z7wBdSk+94mBJMCUFuHvcNaAaj9ijnQv9/EWd6OW0Ut4ozfOPynM/AsXaM0heWQ49zjAHidNHJL3jtnOjUk2XHkKwNLkUSpKkyPAdJ9uTqFC8DmllXWy35q6G/3pi5tV27Ji63tciEqDUGmMoJM2BnINZiFQmUO6k3T7G/QgYYJiQuYMpmAoNVeiJo9QabVbWTCvoZct++g0BowqLcdXE5sMBasPCVN6RnKCNuGQ6uhp9uk2BieVVhJme2detorM8k6irZZAzA2KiaY0FDdFF3HGlu/OY5hTNBE93RBNXEf1dnZA3vpnnbKwERZGKY3LTraFLc2ytDTBXiW0Ch6jM5uBLUZzm9l0KObaTCDF/r+ShQzu056dErHD78OhWZjHR6KFSUmB1Kj7s7VsS/lNv3M4jDoNIFqPa1HF0iNTkqnHO0B9cZPa0cxYESxlAj3ShON/Ux51VHta7u2strm6yp9oAgogiE4QiAshA3KA6u+22axEyMsVv0y1gBlsOgZ3GAzdrPa7VS7tRsAAA==') format('woff2'),
5 | url('iconfont.woff?t=1576828227650') format('woff'),
6 | url('iconfont.ttf?t=1576828227650') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+ */
7 | url('iconfont.svg?t=1576828227650#iconfont') format('svg'); /* iOS 4.1- */
8 | }
9 |
10 | .fa-custom {
11 | font-family: "iconfont" !important;
12 | font-size: 16px;
13 | font-style: normal;
14 | -webkit-font-smoothing: antialiased;
15 | -moz-osx-font-smoothing: grayscale;
16 | }
17 |
18 | .jianshu:before {
19 | content: "\e60c";
20 | }
21 |
22 | .zhihu:before {
23 | content: "\e688";
24 | }
25 |
26 | .wechat:before {
27 | content: "\e502";
28 | }
29 |
30 | .csdn:before {
31 | content: "\e515";
32 | }
33 |
34 | .juejin:before {
35 | content: "\e503";
36 | }
37 |
--------------------------------------------------------------------------------
/source/_posts/@10aaf06.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "爬虫养成记--千军万马来相见(详解多线程)"
3 | description: "在上篇教程《爬虫养成记--顺藤摸瓜回首掏(女生定制篇)》中我们通过分析网页之间的联系,串起一条线,从而爬取大量的小哥哥图片,但是一张一张的爬取速度未免也有些太慢,在本篇教程中将会与大家分享提高爬虫速率的神奇技能——多线程。"
4 | tags: ["爬虫"]
5 | categories: ["后端", "Python", "入门"]
6 | date: 2020-03-23T00:00:00.509Z
7 | photos:
8 | - https://static.tuture.co/c/%4010aaf06.md/spider-3-cover.jpg
9 | ---
10 |
11 |
12 |
13 |

14 |
15 |
20 |
21 |
22 | ## 前情回顾
23 | 在上篇教程[爬虫养成记--顺藤摸瓜回首掏(女生定制篇)](https://blog.csdn.net/crxk_/article/details/104892652)中我们通过分析网页之间的联系,串起一条线,从而爬取大量的小哥哥图片,但是一张一张的爬取速度未免也**有些太慢**,在本篇教程中将会与大家分享**提高爬虫速率**的神奇技能——多线程。
24 |
25 | ## 慢在哪里?
26 | 首先我们将之前所写的爬虫程序以流程图的方式将其表示出来,通过这种更直观的方式来分析程序在速度上的瓶颈。下面*程序流程图*中红色箭头标明了程序获取一张图片时所要执行的步骤。
27 | 
28 | 大多数的程序设计语言其代码执行顺序都是同步执行(JavaScript为异步),也就是说在Python程序中只有上一条语句执行完成了,下一条语句才会开始执行。从流程图中也可以看出来,只有第一页的图片抓取完成了,第二页的图片才会开始下载…………,当整个图集所有的图片都处理完了,下一个图集的图片才会开始进行遍历下载。此过程如*串行流程图*中蓝色箭头所示:
29 | 
30 |
31 | 从图中可以看出当程序入到每个分叉点时也就是进入for循环时,在循环队列中的每个任务(比如遍历图集or下载图片)就只能等着前面一个任务完成,才能开始下面一个任务。就是因为**需要等待**,才拖慢了程序的速度。
32 |
33 | 这就像食堂打饭一样,如果只有一个窗口,每个同学打饭时长为一分钟,那么一百个学生就有99个同学需要等待,100个同学打饭的总时长为1+2+3+……+ 99 + 100 = 5050分钟。如果哪天食堂同时开放了100个窗口,那么100个同学打饭的总时间将变为1分钟,时间缩短了五千多倍!
34 | ## 如何提速?
35 | 我们现在所使用的计算机都拥有多个CPU,就相当于三头六臂的哪吒,完全可以多心多用。如果可以充分发掘计算机的算力,将上述**串行**的执行顺序**改为并行**执行(如下*并行流程图*所示),那么在整个程序的执行的过程中将**消灭等待**的过程,速度会有质的飞跃!
36 | 
37 | ### 从单线程到多线程
38 | 单线程 = 串行
39 | 从串行流程图中可以看出红色箭头与蓝色箭头是首尾相连,一环扣一环。这称之为串行。
40 |
41 | 多线程 = 并行
42 | 从并行流程图中可以看出红色箭头每到一个分叉点就直接产生了分支,多个分支共同执行。此称之为并行。
43 |
44 | 当然在整个程序当中,不可能一开始就搞个并行执行,串行是并行的基础,它们两者相辅相成。只有当程序出现分支(进入for循环)此时多线程可以派上用场,为每一个分支开启一个线程从而加速程序的执行。对于萌新可以粗暴简单地理解:**没有for循环,就不用多线程**。对于有一定编程经验的同学可以这样理解:**当程序中出现耗时操作时,要另开一个线程处理此操作**。所谓耗时操做比如:文件IO、网络IO……。
45 |
46 | ## 动手实践
47 | ### 定义一个线程类
48 | Python3中提供了[threading](https://www.runoob.com/python3/python3-multithreading.html)模块用于帮助用户构建多线程程序。我们首先将基于此模块来自定义一个线程类,用于消灭遍历图集时所需要的等待。
49 | #### 线程ID
50 | 程序执行时会开启很多个线程,为了后期方便管理这些线程,可以在线程类的构造方法中添加threadID这一参数,为每个线程赋予唯一的ID号
51 | #### 所执行目标方法的参数
52 | 一般来说定义一个线程类主要目的是让此线程去执行一个耗时的方法,所以这个线程类的构造方法中所需要传入所要执行目的方法的参数。比如 handleTitleLinks 这个类主要用来执行[getBoys()](https://blog.csdn.net/crxk_/article/details/104892652) (参见文末中的完整代码)这一方法。getBoys() 所需一个标题的链接作为参数,所以在handleTitleLinks的构造方法中也需要传入一个链接。
53 | #### 调用目标方法
54 | 线程类需要一个run(),在此方法中传入参数,调用所需执行的目标方法即可。
55 | ```python
56 | class handleTitleLinks (threading.Thread):
57 | def __init__(self,threadID,link):
58 | threading.Thread.__init__(self)
59 | self.threadID = threadID
60 | self.link = link
61 | def run(self):
62 | print ("start handleTitleLinks:" + self.threadID)
63 | getBoys(self.link)
64 | print ("exit handleTitleLinks:" + self.threadID)
65 | ```
66 | ### 实例化线程对象代替目标方法
67 | 当把线程类定义好之后,找到曾经耗时的目标方法,实例化一个线程对象将其代替即可。
68 |
69 | ```python
70 | def main():
71 | baseUrl = "https://www.nanrentu.cc/sgtp/"
72 | response = requests.get(baseUrl,headers=headers)
73 | if response.status_code == 200:
74 | with open("index.html",'w',encoding="utf-8") as f:
75 | f.write(response.text)
76 | doc = pq(response.text)
77 | # 得到所有图集的标题连接
78 | titleLinks = doc('.h-piclist > li > a').items()
79 | # 遍历这些连接
80 | for link in titleLinks:
81 | # 替换目标方法,开启线程
82 | handleTitleLinks(uuid.uuid1().hex,link).start()
83 | # getBoys(link)
84 | ```
85 | ### 如法炮制
86 | 我们已经定义了一个线程去处理每个图集,但是在处理每个图集的过程中还会有分支(参见程序并行执行图)去下载图集中的图片。此时需要再定义一个线程用来下载图片,即定义一个线程去替换getImg()。
87 |
88 | ```python
89 | class handleGetImg (threading.Thread):
90 | def __init__(self,threadID,urlArray):
91 | threading.Thread.__init__(self)
92 | self.threadID = threadID
93 | self.url = url
94 | def run(self):
95 | print ("start handleGetImg:" + self.threadID)
96 | getPic(self.urlArray)
97 | print ("exit handleGetImg:" + self.threadID)
98 | ```
99 | ### 改造后完整代码如下:
100 |
101 | ```python
102 | #!/usr/bin/python3
103 | import requests
104 | from pyquery import PyQuery as pq
105 | import uuid
106 | import threading
107 |
108 | headers = {
109 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36',
110 | 'cookie': 'UM_distinctid=170a5a00fa25bf-075185606c88b7-396d7407-100200-170a5a00fa3507; CNZZDATA1274895726=1196969733-1583323670-%7C1583925652; Hm_lvt_45e50d2aec057f43a3112beaf7f00179=1583326696,1583756661,1583926583; Hm_lpvt_45e50d2aec057f43a3112beaf7f00179=1583926583'
111 | }
112 | def saveImage(imgUrl,name):
113 | imgResponse = requests.get(imgUrl)
114 | fileName = "学习文件/%s.jpg" % name
115 | if imgResponse.status_code == 200:
116 | with open(fileName, 'wb') as f:
117 | f.write(imgResponse.content)
118 | f.close()
119 |
120 | # 根据链接找到图片并下载
121 | def getImg(url):
122 | res = requests.get(url,headers=headers)
123 | if res.status_code == 200:
124 | doc = pq(res.text)
125 | imgSrc = doc('.info-pic-list > a > img').attr('src')
126 | print(imgSrc)
127 | saveImage(imgSrc,uuid.uuid1().hex)
128 |
129 | # 遍历组图链接
130 | def getPic(urlArray):
131 | for url in urlArray:
132 | # 替换方法
133 | handleGetImg(uuid.uuid1().hex,url).start()
134 | # getImg(url)
135 |
136 |
137 | def createUrl(indexUrl,allPage):
138 | baseUrl = indexUrl.split('.html')[0]
139 | urlArray = []
140 | for i in range(1,allPage):
141 | tempUrl = baseUrl+"_"+str(i)+".html"
142 | urlArray.append(tempUrl)
143 | return urlArray
144 |
145 | def getBoys(link):
146 | # 摸瓜第1步:获取首页连接
147 | picIndex = link.attr('href')
148 | # 摸瓜第2步:打开首页,提取末页链接,得出组图页数
149 | res = requests.get(picIndex,headers=headers)
150 | print("当前正在抓取的 picIndex: " + picIndex)
151 | if res.status_code == 200:
152 | with open("picIndex.html",'w',encoding="utf-8") as f:
153 | f.write(res.text)
154 | doc = pq(res.text)
155 | lastLink = doc('.page > ul > li:nth-last-child(2) > a').attr('href')
156 | # 字符串分割,得出全部的页数
157 | if(lastLink is None):
158 | return
159 | # 以.html 为分割符进行分割,取结果数组中的第一项
160 | temp = lastLink.split('.html')[0]
161 | # 再以下划线 _ 分割,取结果数组中的第二项,再转为数值型
162 | allPage = int(temp.split('_')[1])
163 | # 摸瓜第3步:根据首尾链接构造url
164 | urlArray = createUrl(picIndex,allPage)
165 | # 摸瓜第4步:存储图片,摸瓜成功
166 | getPic(urlArray)
167 |
168 | def main():
169 | baseUrl = "https://www.nanrentu.cc/sgtp/"
170 | response = requests.get(baseUrl,headers=headers)
171 | if response.status_code == 200:
172 | with open("index.html",'w',encoding="utf-8") as f:
173 | f.write(response.text)
174 | doc = pq(response.text)
175 | # 得到所有图集的标题连接
176 | titleLinks = doc('.h-piclist > li > a').items()
177 | # 遍历这些连接
178 | for link in titleLinks:
179 | # 替换方法,开启线程
180 | handleTitleLinks(uuid.uuid1().hex,link).start()
181 | # getBoys(link)
182 |
183 | # 处理组图链接的线程类
184 | class handleTitleLinks (threading.Thread):
185 | def __init__(self,threadID,link):
186 | threading.Thread.__init__(self)
187 | self.threadID = threadID
188 | self.link = link
189 | def run(self):
190 | print ("start handleTitleLinks:" + self.threadID)
191 | getBoys(self.link)
192 | print ("exit handleTitleLinks:" + self.threadID)
193 | # 下载图片的线程类
194 | class handleGetImg (threading.Thread):
195 | def __init__(self,threadID,url):
196 | threading.Thread.__init__(self)
197 | self.threadID = threadID
198 | self.url = url
199 | def run(self):
200 | print ("start handleGetImg:" + self.threadID)
201 | getImg(self.url)
202 | print ("exit handleGetImg:" + self.threadID)
203 |
204 | if __name__ == "__main__":
205 | main()
206 | ```
207 | ## 性能对比
208 | 
209 | 
210 | 
211 |
212 | 因为网络波动的原因,采用多线程后并不能获得理论上的速度提升,不过显而易见的时多线程能大幅度提升程序速度,且数据量越大效果越明显。
213 |
214 | ## 总结
215 | 至此爬虫养成记系列文章,可以告一段落了。我们从零开始一步一步地学习了如何获取网页,然后从中分析出所要下载的图片;还学习了如何分析网页之间的联系,从而获取到更多的图片;最后又学习了如何利用多线程提高程序运行的效率。
216 |
217 | 希望各位看官能从这三篇文章中获得启发,体会到分析、设计并实现爬虫程序时的各种方法与思想,从而能够举一反三,写出自己所需的爬虫程序~ 加油!🆙💪
218 |
219 | ## 预告
220 | 敬请期待爬虫进阶记~
--------------------------------------------------------------------------------
/source/_posts/@2629c44.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "爬虫养成记--顺藤摸瓜回首掏(女生定制篇)"
3 | description: "在上篇教程[爬虫养成记——先跨进这个精彩的世界(女生定制篇)]中我们已经可以将所有小哥哥的封面照片抓取下来,但仅仅是封面图片在质量和数量上怎么能满足小仙女们的要求呢?在本篇教程中,我们串起一根姻缘“线”,来把这一系列的小哥哥们都收入囊中。"
4 | tags: ["爬虫"]
5 | categories: ["后端", "Python", "入门"]
6 | date: 2020-03-16T00:00:00.509Z
7 | photos:
8 | - https://static.tuture.co/c/%402629c44.md/spider-2-cover.jpg
9 | ---
10 |
11 |
12 |
13 |

14 |
15 |
20 |
21 |
22 | ## 出门先化妆
23 |
24 | 小仙女们出门约会总会“淡妆浓抹总相宜”,那爬虫出门去爬取数据,也得打扮打扮啊,不然怎么能让男神们都乖乖地跟着走呢?
25 |
26 | 爬虫的“化妆”可不是“妆前乳 --> 粉底 --> 遮瑕 --> 散粉 --> 画眉 --> 口红”等这些步骤,其目的是为了让对方网站更加确信来访者不是爬虫程序,而是一个活生生的人。人们通过操控浏览器来访问网站,那么爬虫程序只需要模仿浏览器就可以了。 那就来看看浏览器在打开网页时都画了那些“妆”。
27 |
28 | 
29 |
30 | 打开Chrome并打开调试台,切换到NetWork选项卡,此时访问 https://www.nanrentu.cc/sgtp/ , 这是时候会看到调试台里出现了很多链接信息,这么多链接到底哪个是我们所需要的呢?回想一下上一篇内容,首先是要获得HTML文档,再从此文档中提取出图片的链接,所以目标有了,就是找到浏览器获取到这个HTML文档的那个链接。
31 |
32 | Chrome知道这么多链接信息肯定会让开发者陷入茫然,所以给链接进行了归类,点击上方Doc分类,再点击那唯一的一条链接,就会看到获取此HTML文档链接的详细信息了。此时我们关注主要Request Headers 这个里面的内容。浏览器通过http协议与服务器交互获取信息,爬虫是通过模仿浏览器发出http协议获取信息,其中最重要的一个模仿点就是Request Headers。
33 |
34 | ### http协议里面的“瓶瓶罐罐”
35 | 让男生看女孩子化妆用的那些瓶瓶罐罐估计会陷入沉思,这是BB霜,那是粉底液,还有散粉、眼影、遮瑕膏,更不用说各种色号的口红啦。那女孩子看到这http里面的各项内容时估计也会一脸懵逼,其这比化妆品简单多了,我们只需简单了解,就能给爬虫画出精致妆容。
36 |
37 | ```
38 | :authority: www.nanrentu.cc
39 | :method: GET // 自定义请求头 请求方法
40 | :path: /sgtp/ // 自定义请求头 请求路径
41 | :scheme: https // 自定义请求头 请求方式
42 | // 所接受的内容格式
43 | accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
44 | // 所接受的编码方式
45 | accept-encoding: gzip, deflate, br
46 | // 所接受的语言
47 | accept-language: zh-CN,zh;q=0.9
48 | // 缓存控制:告诉服务器客户端希望得到一个最新的资源
49 | cache-control: max-age=0
50 | cookie: UM_distinctid=170a5a00fa25bf-075185606c88b7-396d7407-100200-170a5a00fa3507; Hm_lvt_45e50d2aec057f43a3112beaf7f00179=1583326696,1583756661; CNZZDATA1274895726=1196969733-1583323670-%7C1583752625; Hm_lpvt_45e50d2aec057f43a3112beaf7f00179=1583756721
51 | sec-fetch-dest: document
52 | sec-fetch-mode: navigate
53 | sec-fetch-site: none
54 | sec-fetch-user: ?1
55 | // 屏蔽HTTPS页面出现HTTP请求警报
56 | upgrade-insecure-requests: 1
57 | user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36
58 | ```
59 | 这么多的信息不用都给爬虫加上,因为这网站的防爬措施等级不高,暂时只要关键的两个就可以了。
60 | - cookie: 这是存储在浏览器里面一段文本,有时包含了验证信息和一些特殊的请求信息
61 | - user-agent:用于标识此请求是由什么工具所发出的
62 | 关于User-Agent的详细信息可以参考此篇博文 [谈谈 UserAgent 字符串的规律和伪造方法](https://juejin.im/entry/59cf793a51882550b219567b)
63 |
64 | 但是当爬取其他网站时可能会有所需要,在这里赘述这么多原因就是希望大家能明白**伪装爬虫**的重要性,以及怎么获取这些伪装信息。
65 |
66 |
67 |
68 | ```python
69 | // 建立一个名叫headers的字典
70 | headers = {
71 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36',
72 | 'cookie': 'UM_distinctid=170a5a00fa25bf-075185606c88b7-396d7407-100200-170a5a00fa3507; CNZZDATA1274895726=1196969733-1583323670-%7C1583925652; Hm_lvt_45e50d2aec057f43a3112beaf7f00179=1583326696,1583756661,1583926583; Hm_lpvt_45e50d2aec057f43a3112beaf7f00179=1583926583'
73 | }
74 | // 发送请求时带上请求头
75 | response = requests.get(baseUrl,headers=headers)
76 | ```
77 |
78 |
79 |
80 | ## 顺藤摸瓜
81 | 一个网站是由若干个网页组合而成的,网页中充满着各种超链接,从这网页链接到那个网页,如果我们想要更多小哥哥,那就得首先分析出串联起他们那些超链接,然后就可以顺藤摸瓜咯。
82 |
83 | 
84 |
85 | 当把鼠标发放到标题上时,标题的颜色发生了变化,证明这一元素为超连接,点击标题浏览器会自动打开一个tab标签页,来显示网页,注意到下方的页码标签,是这些元素串联起了整个图集。
86 |
87 | 
88 |
89 | 点击“末页”观察url发生了什么变化
90 |
91 | 末页的url:https://www.nanrentu.cc/sgtp/36805_7.html
92 |
93 | 首页的url:https://www.nanrentu.cc/sgtp/36805.html
94 |
95 | 看起来有点意思了,末页的url比首页的url多了“_7”,接下来再点击分别进入第2页,第3页……观察url的变化,可得出下表。
96 |
97 |
98 | 页面 | url
99 | ---|---
100 | 首页 | https://www.nanrentu.cc/sgtp/36805.html
101 | 第2页 | https://www.nanrentu.cc/sgtp/36805_2.html
102 | 第3页 | https://www.nanrentu.cc/sgtp/36805_3.html
103 | 第4页 | https://www.nanrentu.cc/sgtp/36805_4.html
104 | 第5页 | https://www.nanrentu.cc/sgtp/36805_5.html
105 | 第6页 | https://www.nanrentu.cc/sgtp/36805_6.html
106 | 第7页 | https://www.nanrentu.cc/sgtp/36805_7.html
107 |
108 | 多点几个组图,也会发现同样规律。这样就明了很多了,我们已经分析清楚了这个跟“藤”的开头与结尾,接下来就可以敲出代码让爬虫开始“摸瓜”咯。
109 | ### 摸瓜第1步:提取标题链接
110 |
111 | 这个操作与上篇博文中所介绍的一样,打开调试台切换到Elements选项卡就能开始探索提取了。
112 |
113 | 
114 |
115 | ### 摸瓜第2步:提取末页链接,得出组图页数
116 |
117 | 
118 |
119 | 通过观察HTML元素结构,可发现包含末页的 \
标签为其父元素\的倒数第二个子元素,所以可得出以下的css选择器
120 |
121 | .page > ul > li:nth-last-child(2) > a
122 |
123 | ### 摸瓜第3步:根据首尾链接构造url
124 | 为了构造url更加方便,我们可以把首页 https://www.nanrentu.cc/sgtp/36805.html 变为 https://www.nanrentu.cc/sgtp/36805_1.html, 在浏览器中打开带有后缀的这个网址,依然能够成功访问到首页,不要问我为什么?这可能就是程序员之间的一种默契吧~
125 | ### 摸瓜第4步:存储图片,摸瓜成功
126 |
127 | 完整的代码如下:
128 |
129 | ```python
130 | import requests
131 | from pyquery import PyQuery as pq
132 | import uuid
133 |
134 | headers = {
135 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36',
136 | 'cookie': 'UM_distinctid=170a5a00fa25bf-075185606c88b7-396d7407-100200-170a5a00fa3507; CNZZDATA1274895726=1196969733-1583323670-%7C1583925652; Hm_lvt_45e50d2aec057f43a3112beaf7f00179=1583326696,1583756661,1583926583; Hm_lpvt_45e50d2aec057f43a3112beaf7f00179=1583926583'
137 | }
138 | def saveImage(imgUrl,name):
139 | imgResponse = requests.get(imgUrl)
140 | fileName = "学习文件/%s.jpg" % name
141 | if imgResponse.status_code == 200:
142 | with open(fileName, 'wb') as f:
143 | f.write(imgResponse.content)
144 | f.close()
145 |
146 | def getPic(urlArray):
147 | for url in urlArray:
148 | res = requests.get(url,headers=headers)
149 | if res.status_code == 200:
150 | doc = pq(res.text)
151 | imgSrc = doc('.info-pic-list > a > img').attr('src')
152 | print(imgSrc)
153 | saveImage(imgSrc,uuid.uuid1().hex)
154 |
155 |
156 | def createUrl(indexUrl,allPage):
157 | baseUrl = indexUrl.split('.html')[0]
158 | urlArray = []
159 | for i in range(1,allPage):
160 | tempUrl = baseUrl+"_"+str(i)+".html"
161 | urlArray.append(tempUrl)
162 | return urlArray
163 |
164 | def getBoys(link):
165 | # 摸瓜第1步:获取首页连接
166 | picIndex = link.attr('href')
167 | # 摸瓜第2步:打开首页,提取末页链接,得出组图页数
168 | res = requests.get(picIndex,headers=headers)
169 | print("当前正在抓取的 picIndex: " + picIndex)
170 | if res.status_code == 200:
171 | with open("picIndex.html",'w',encoding="utf-8") as f:
172 | f.write(res.text)
173 | doc = pq(res.text)
174 | lastLink = doc('.page > ul > li:nth-last-child(2) > a').attr('href')
175 | # 字符串分割,得出全部的页数
176 | if(lastLink is None):
177 | return
178 | # 以.html 为分割符进行分割,取结果数组中的第一项
179 | temp = lastLink.split('.html')[0]
180 | # 再以下划线 _ 分割,取结果数组中的第二项,再转为数值型
181 | allPage = int(temp.split('_')[1])
182 | # 摸瓜第3步:根据首尾链接构造url
183 | urlArray = createUrl(picIndex,allPage)
184 | # 摸瓜第4步:存储图片,摸瓜成功
185 | getPic(urlArray)
186 |
187 | def main():
188 | baseUrl = "https://www.nanrentu.cc/sgtp/"
189 | response = requests.get(baseUrl,headers=headers)
190 | if response.status_code == 200:
191 | with open("index.html",'w',encoding="utf-8") as f:
192 | f.write(response.text)
193 | doc = pq(response.text)
194 | # 得到所有图集的标题连接
195 | titleLinks = doc('.h-piclist > li > a').items()
196 | # 遍历这些连接
197 | for link in titleLinks:
198 | getBoys(link)
199 |
200 | if __name__ == "__main__":
201 | main()
202 | ```
203 |
204 | 运行结果:
205 |
206 | 
207 |
208 | ## 回首掏
209 |
210 | 回顾整个爬虫程序,它是连续式流水线作业,每一步之间都是环环相扣,所以在写程序前自己一定要把整个流水线的每个环节都考虑清楚,把它们之间的顺序依赖关系化成一个简易的流程图,对着流程图再写程序就会清晰很多。我们可以把每一个模块都写成一个函数,先对函数做好单元测试,再把这些函数按顺序组合起来就行啦。分而治之,有机组合这就是编程的奥义。再复杂的项目,都是由一个个模块组建起来的,这和搭积木是一样的道理。
211 |
212 | 
213 |
214 | 这个流程图只用单项箭头画出了获取一张图片的全部过程,这就相当于一个工人在干活,我们的计算机是一个大工厂里面有成千上万个工人,只让一个工人干活其他的人都在为他加油嘛?那也太说不过去,在下一篇文章中,我们将画出完整的流程图,分析出其他工人没活干的原因,然后充分调动起计算机的算力,来提升程序的运行效率。
215 |
--------------------------------------------------------------------------------
/source/_posts/@34dvBzFh6.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 'Nest.js 从零到壹系列(二):使用 Sequelize 操作数据库'
3 | description: '上一篇介绍了如何创建项目、路由的访问以及如何创建模块,这篇来讲讲数据库的连接与使用。'
4 | tags: ['Nest.js']
5 | categories: ['后端', 'Node.js', '进阶']
6 | date: 2020-05-12T00:01:00.509Z
7 | photos:
8 | - https://static.tuture.co/c/%4034dvBzFh6/2.jpg
9 | ---
10 |
11 |
12 |
13 |

14 |
15 |
20 |
21 |
22 | ## 前言
23 |
24 | 上一篇介绍了如何创建项目、路由的访问以及如何创建模块,这篇来讲讲数据库的连接与使用。
25 |
26 | 既然是后端项目,当然要能连上数据库,否则还不如直接写静态页面。
27 |
28 | 本教程使用的是 MySQL,有人可能会问为啥不用 MongoDB。。。呃,因为公司使用 MySQL,我也是结合项目经历写的教程,MongoDB 还没踩过坑,所以就不在这误人子弟了。
29 |
30 | [GitHub 项目地址](https://github.com/SephirothKid/nest-zero-to-one),欢迎各位大佬 Star。
31 |
32 | ## 一、MySQL 准备
33 |
34 | 首先要确保你有数据库可以连接,如果没有,可以在 MySQL 官网下载一个,本地跑起来。安装教程这里就不叙述了,“百度一下,你就知道”。
35 |
36 | 推荐使用 Navicat Premium 可视化工具来管理数据库。
37 |
38 | 用 Navicat 连接上数据库后,新建一个库:
39 |
40 | 
41 |
42 | 
43 |
44 | 点开我们刚创建的库 `nest_zero_to_one`,点开 Tables,发现里面空空如也,接下来我们创建一张新表,点开上面工具栏的 Query,并新增查询:
45 |
46 | 
47 |
48 | 将下列代码复制到框内,点击上面的运行,即可完成表的创建:
49 |
50 | ```ts
51 | CREATE TABLE `admin_user` (
52 | `user_id` smallint(6) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
53 | `account_name` varchar(24) NOT NULL COMMENT '用户账号',
54 | `real_name` varchar(20) NOT NULL COMMENT '真实姓名',
55 | `passwd` char(32) NOT NULL COMMENT '密码',
56 | `passwd_salt` char(6) NOT NULL COMMENT '密码盐',
57 | `mobile` varchar(15) NOT NULL DEFAULT '0' COMMENT '手机号码',
58 | `role` tinyint(4) NOT NULL DEFAULT '3' COMMENT '用户角色:0-超级管理员|1-管理员|2-开发&测试&运营|3-普通用户(只能查看)',
59 | `user_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '状态:0-失效|1-有效|2-删除',
60 | `create_by` smallint(6) NOT NULL COMMENT '创建人ID',
61 | `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
62 | `update_by` smallint(6) NOT NULL DEFAULT '0' COMMENT '修改人ID',
63 | `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
64 | PRIMARY KEY (`user_id`),
65 | KEY `idx_m` (`mobile`)
66 | ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='后台用户表';
67 |
68 | ```
69 |
70 | 
71 |
72 | 然后我们可以看到,左边的 `Tables` 下多出了 `admin_user` 表,点开就可以看到字段信息了:
73 |
74 | 
75 |
76 | 我们先随便插入 2 条数据,方便后面的查询:
77 |
78 | 
79 |
80 | ## 二、项目的数据库配置
81 |
82 | 先在项目根目录创建一个文件夹 `config`(与 `src` 同级),专门放置各种配置。
83 |
84 | 然后新建一个文件 `db.ts`:
85 |
86 | ```ts
87 | // config/db.ts
88 | const productConfig = {
89 | mysql: {
90 | port: '数据库端口',
91 | host: '数据库地址',
92 | user: '用户名',
93 | password: '密码',
94 | database: 'nest_zero_to_one', // 库名
95 | connectionLimit: 10, // 连接限制
96 | },
97 | };
98 |
99 | const localConfig = {
100 | mysql: {
101 | port: '数据库端口',
102 | host: '数据库地址',
103 | user: '用户名',
104 | password: '密码',
105 | database: 'nest_zero_to_one', // 库名
106 | connectionLimit: 10, // 连接限制
107 | },
108 | };
109 |
110 | // 本地运行是没有 process.env.NODE_ENV 的,借此来区分[开发环境]和[生产环境]
111 | const config = process.env.NODE_ENV ? productConfig : localConfig;
112 |
113 | export default config;
114 | ```
115 |
116 | > Ps:这个文件是不同步到 github 的,需要各位读者结合实际情况配置
117 |
118 | 市面上有很多连接数据库的工具,笔者这里使用的是 `Sequelize`,先安装依赖包:
119 |
120 | ```ts
121 | $ npm i sequelize sequelize-typescript mysql2 -S
122 | 或
123 | $ yarn add sequelize sequelize-typescript mysql2 -S
124 |
125 | ```
126 |
127 | 然后在 `src` 目录下创建文件夹 `database`,然后再创建 `sequelize.ts`:
128 |
129 | ```ts
130 | // src/database/sequelize.ts
131 | import { Sequelize } from 'sequelize-typescript';
132 | import db from '../../config/db';
133 |
134 | const sequelize = new Sequelize(
135 | db.mysql.database,
136 | db.mysql.user,
137 | db.mysql.password || null,
138 | {
139 | // 自定义主机; 默认值: localhost
140 | host: db.mysql.host, // 数据库地址
141 | // 自定义端口; 默认值: 3306
142 | port: db.mysql.port,
143 | dialect: 'mysql',
144 | pool: {
145 | max: db.mysql.connectionLimit, // 连接池中最大连接数量
146 | min: 0, // 连接池中最小连接数量
147 | acquire: 30000,
148 | idle: 10000, // 如果一个线程 10 秒钟内没有被使用过的话,那么就释放线程
149 | },
150 | timezone: '+08:00', // 东八时区
151 | },
152 | );
153 |
154 | // 测试数据库链接
155 | sequelize
156 | .authenticate()
157 | .then(() => {
158 | console.log('数据库连接成功');
159 | })
160 | .catch((err: any) => {
161 | // 数据库连接失败时打印输出
162 | console.error(err);
163 | throw err;
164 | });
165 |
166 | export default sequelize;
167 | ```
168 |
169 | ## 三、数据库连接测试
170 |
171 | 好了,接下来我们来测试一下数据库的连接情况。
172 |
173 | 我们重写 `user.service.ts` 的逻辑:
174 |
175 | ```ts
176 | // src/logical/user/user.service.ts
177 | import { Injectable } from '@nestjs/common';
178 | import * as Sequelize from 'sequelize'; // 引入 Sequelize 库
179 | import sequelize from '../../database/sequelize'; // 引入 Sequelize 实例
180 |
181 | @Injectable()
182 | export class UserService {
183 | async findOne(username: string): Promise {
184 | const sql = `
185 | SELECT
186 | user_id id, real_name realName, role
187 | FROM
188 | admin_user
189 | WHERE
190 | account_name = '${username}'
191 | `; // 一段平淡无奇的 SQL 查询语句
192 | try {
193 | const res = await sequelize.query(sql, {
194 | type: Sequelize.QueryTypes.SELECT, // 查询方式
195 | raw: true, // 是否使用数组组装的方式展示结果
196 | logging: true, // 是否将 SQL 语句打印到控制台,默认为 true
197 | });
198 | const user = res[0]; // 查出来的结果是一个数组,我们只取第一个。
199 | if (user) {
200 | return {
201 | code: 200, // 返回状态码,可自定义
202 | data: {
203 | user,
204 | },
205 | msg: 'Success',
206 | };
207 | } else {
208 | return {
209 | code: 600,
210 | msg: '查无此人',
211 | };
212 | }
213 | } catch (error) {
214 | return {
215 | code: 503,
216 | msg: `Service error: ${error}`,
217 | };
218 | }
219 | }
220 | }
221 | ```
222 |
223 | 保存文件,就会看到控制台刷新了(前提是使用 `yarn start:dev` 启动的),并打印下列语句:
224 |
225 | 
226 |
227 | 这说明之前的配置生效了,我们试着用之前的参数请求一下接口:
228 |
229 | 
230 |
231 | 返回“查无此人”,说明数据库没有叫“Kid”的用户。
232 |
233 | 我们改成正确的已存在的用户名再试试:
234 |
235 | 
236 |
237 | 然后观察一下控制台,我们的查询语句已经打印出来了,通过 `logging: true`,可以在调试 Bug 的时候,更清晰的查找 SQL 语句的错误,不过建议测试稳定后,上线前关闭,不然记录的日志会很繁杂:
238 |
239 | 
240 |
241 | 再对照一下数据库里的表,发现查出来的数据和数据库里的一致,至此,MySQL 连接测试完成,以后就可以愉快的在 Service 里面搬砖了。
242 |
243 | ## 总结
244 |
245 | 这篇介绍了 MySQL 的数据准备、Sequelize 的配置、Nest 怎么通过 Sequelize 连接上 MySQL,以及用一条简单的查询语句去验证连接情况。
246 |
247 | 在这里,**强烈建议使用写原生 SQL 语句去操作数据库**。
248 |
249 | 虽然 Sequelize 提供了很多便捷的方法,具体可去 [Sequelize v5 官方文档](https://sequelize.org/v5/) 浏览学习。但笔者通过观察 `logging` 打印出来的语句发现,其实多了很多无谓的操作,在高并发的情况下,太影响性能了。
250 |
251 | 而且如果不使用原生查询,那么就要建立对象映射到数据库表,然后每次工具更新,还要花时间成本去学习,如果数据库改了字段,那么映射关系就会出错,然后项目就会疯狂报错以致宕机(亲身经历)。
252 |
253 | 而使用原生 SQL,只需要学一种语言就够了,换个工具,也能用,而且就算改了字段,也只会在请求接口的时候报错,到时候再针对那个语句修改就好了,而且现在查找替换功能这么强大,批量修改也不是难事。
254 |
255 | 最重要的是,如果你是从前端转后端,或者根本就是 0 基础到后端,还是建议先把 SQL 的基础打牢,不然连 `JOIN`、`LEFT JOIN` 和 `RIGHT JOIN` 的区别都分不清(我们公司就有个三年经验的后端,乱用 `LEFT JOIN`,然后被 DB 主管一顿痛骂。。。真事儿)。
256 |
257 | 多写、多分析、多看控制台报错、多从性能上考虑,才是最快入门的途径。
258 |
259 | > 注意:在写 UPDATE 更新语句的时候,一定要加上 WHERE 条件,一定要加上 WHERE 条件,一定要加上 WHERE 条件,重要的事情说 3 遍,血与泪的教训!!!
260 |
261 | 
262 |
263 | 下一篇,将介绍如何使用 JWT(Json Web Token)进行单点登录。
264 |
265 | > 本篇收录于[NestJS 实战教程](https://juejin.im/collection/5e893a1b6fb9a04d65a15400),更多文章敬请关注。
266 |
267 | `
268 |
--------------------------------------------------------------------------------
/source/_posts/@4675l54tY.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 'Nest.js 从零到壹系列(七):讨厌写文档,Swagger UI 了解一下?'
3 | description: '上一篇介绍了如何使用寥寥几行代码就实现 RBAC 0,解决了权限管理的痛点,这篇将解决另一个痛点:写文档。'
4 | tags: ['Nest.js']
5 | categories: ['后端', 'Node.js', '进阶']
6 | date: 2020-05-12T00:06:00.509Z
7 | photos:
8 | - https://static.tuture.co/c/%404675l54tY/7.jpg
9 | ---
10 |
11 |
12 |
13 |

14 |
15 |
20 |
21 |
22 | ## 前言
23 |
24 | 上一篇介绍了如何使用寥寥几行代码就实现 RBAC 0,解决了权限管理的痛点,这篇将解决另一个痛点:写文档。
25 |
26 | 上家公司在恒大的时候,项目的后端文档使用 Swagger UI 来展示,这是一个遵循 RESTful API 的、 可以互动的文档,所见即所得。
27 |
28 | 然后进入了目前的公司,接口文档是用 Markdown 写的,并保存在 SVN 上,每次接口修改,都要更新文档,并同步到 SVN,然后前端再拉下来更新。
29 |
30 | 这些都还好,之前还有直接丢个 .doc 文档过来的。。。。
31 |
32 | 以前我总吐槽后端太懒,文档都不愿更新,直到自己写后端时,嗯,真香。。。于是,为了不耽误摸鱼时间,寻找一个趁手的文档工具,就提上日程了。
33 |
34 | [GitHub 项目地址](https://github.com/SephirothKid/nest-zero-to-one),欢迎各位大佬 Star。
35 |
36 | ## 什么是 RESTful API
37 |
38 | [怎样用通俗的语言解释 REST,以及 RESTful ? - 覃超的回答 - 知乎](https://www.zhihu.com/question/28557115/answer/48094438)
39 |
40 | ## Swagger 之旅
41 |
42 | ### 初始化 Swagger
43 |
44 | ```ts
45 | $ yarn add @nestjs/swagger swagger-ui-express -S
46 |
47 | ```
48 |
49 | 安装完依赖包后,只需要在 main.ts 中引入,并设置一些基本信息即可:
50 |
51 | ```ts
52 | // src/main.ts
53 | import { NestFactory } from '@nestjs/core';
54 | import { AppModule } from './app.module';
55 | import * as express from 'express';
56 | import { logger } from './middleware/logger.middleware';
57 | import { TransformInterceptor } from './interceptor/transform.interceptor';
58 | import { HttpExceptionFilter } from './filter/http-exception.filter';
59 | import { AllExceptionsFilter } from './filter/any-exception.filter';
60 | import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
61 |
62 | async function bootstrap() {
63 | const app = await NestFactory.create(AppModule);
64 | app.use(express.json()); // For parsing application/json
65 | app.use(express.urlencoded({ extended: true })); // For parsing application/x-www-form-urlencoded
66 | // 监听所有的请求路由,并打印日志
67 | app.use(logger);
68 | // 使用拦截器打印出参
69 | app.useGlobalInterceptors(new TransformInterceptor());
70 | app.setGlobalPrefix('nest-zero-to-one');
71 | app.useGlobalFilters(new AllExceptionsFilter());
72 | app.useGlobalFilters(new HttpExceptionFilter());
73 | // 配置 Swagger
74 | const options = new DocumentBuilder()
75 | .setTitle('Nest zero to one')
76 | .setDescription('The nest-zero-to-one API description')
77 | .setVersion('1.0')
78 | .addTag('test')
79 | .build();
80 | const document = SwaggerModule.createDocument(app, options);
81 | SwaggerModule.setup('api-doc', app, document);
82 |
83 | await app.listen(3000);
84 | }
85 | bootstrap();
86 | ```
87 |
88 | 接下来,我们访问 `localhost:3000/api-doc/#/` (假设你的端口是 3000),不出意外,会看到下图:
89 |
90 | 
91 |
92 | 这就是 Swagger UI,页面列出了我们之前写的 `Router` 和 `DTO`(即图中的 Schemas)
93 |
94 | ### 映射 DTO
95 |
96 | 点开 `RegisterInfoDTO`,发现里面是空的,接下来,我们配置一下参数信息,在 `user.dto.ts` 中引入 `ApiProperty`,然后添加到之前的 `class-validator` 上:
97 |
98 | ```ts
99 | // src/logical/user/user.dto.ts
100 | import { IsNotEmpty, IsNumber, IsString } from 'class-validator';
101 | import { ApiProperty } from '@nestjs/swagger';
102 |
103 | export class RegisterInfoDTO {
104 | @ApiProperty()
105 | @IsNotEmpty({ message: '用户名不能为空' })
106 | readonly accountName: string;
107 | @ApiProperty()
108 | @IsNotEmpty({ message: '真实姓名不能为空' })
109 | @IsString({ message: '真实姓名必须是 String 类型' })
110 | readonly realName: string;
111 | @ApiProperty()
112 | @IsNotEmpty({ message: '密码不能为空' })
113 | readonly password: string;
114 | @ApiProperty()
115 | @IsNotEmpty({ message: '重复密码不能为空' })
116 | readonly repassword: string;
117 | @ApiProperty()
118 | @IsNotEmpty({ message: '手机号不能为空' })
119 | @IsNumber()
120 | readonly mobile: number;
121 | @ApiProperty()
122 | readonly role?: string | number;
123 | }
124 | ```
125 |
126 | 保存,**刷新页面**(该页面没有热加载功能),再看看效果:
127 |
128 | 
129 |
130 | 看到已经有了字段信息了,但是我们的 `role` 字段是【可选】的,而文档中是【必填】的,接下来再完善一下描述:
131 |
132 | ```ts
133 | // src/logical/user/user.dto.ts
134 | @ApiProperty({
135 | required: false,
136 | description: '[用户角色]: 0-超级管理员 | 1-管理员 | 2-开发&测试&运营 | 3-普通用户(只能查看)',
137 | })
138 | readonly role?: number | string;
139 |
140 | ```
141 |
142 | 
143 |
144 | 其实,我们可以使用 `ApiPropertyOptional` 装饰器来表示【可选】参数,这样就不用频繁写 `required: false` 了:
145 |
146 | ```ts
147 | // src/logical/user/user.dto.ts
148 | import { IsNotEmpty, IsNumber, IsString } from 'class-validator';
149 | import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
150 |
151 | export class RegisterInfoDTO {
152 | ...
153 | @ApiPropertyOptional({
154 | description: '[用户角色]: 0-超级管理员 | 1-管理员 | 2-开发&测试&运营 | 3-普通用户(只能查看)',
155 | })
156 | readonly role?: number | string;
157 | }
158 |
159 | ```
160 |
161 | ### 接口标签分类
162 |
163 | 通过前面的截图可以看到,所有的接口都在 Default 栏目下,接口多了之后,就很不方便查找了。
164 |
165 | 我们可以根据 Controller 来分类,添加装饰器 `@ApiTags` 即可:
166 |
167 | ```ts
168 | // src/logical/user/user.controller.ts
169 | import { Controller, Post, Body, UseGuards, UsePipes } from '@nestjs/common';
170 | import { AuthGuard } from '@nestjs/passport';
171 | import { AuthService } from '../auth/auth.service';
172 | import { UserService } from './user.service';
173 | import { ValidationPipe } from '../../pipe/validation.pipe';
174 | import { RegisterInfoDTO } from './user.dto';
175 | import { ApiTags } from '@nestjs/swagger';
176 |
177 | @ApiTags('user') // 添加 接口标签 装饰器
178 | @Controller('user')
179 | export class UserController {
180 | constructor(private readonly authService: AuthService, private readonly usersService: UserService) {}
181 |
182 | // JWT验证 - Step 1: 用户请求登录
183 | @Post('login')
184 | async login(@Body() loginParmas: any) {
185 | ...
186 | }
187 |
188 | @UseGuards(AuthGuard('jwt'))
189 | @UsePipes(new ValidationPipe())
190 | @Post('register')
191 | async register(@Body() body: RegisterInfoDTO) {
192 | return await this.usersService.register(body);
193 | }
194 | }
195 |
196 | ```
197 |
198 | 保存再刷新一下页面,看到用户相关的都在一个栏目下了:
199 |
200 | 
201 |
202 | ### 在 Swagger 中登录
203 |
204 | 接下来,我们测试一下注册接口的请求,先编辑参数,然后点击 Execute:
205 |
206 | 
207 |
208 | 然后看一下返回参数:
209 |
210 | 
211 |
212 | 看到返回的是 401 未登录。
213 |
214 | 那么,如何在 Swagger 中登录呢?
215 |
216 | 我们先完善登录接口的 DTO:
217 |
218 | ```ts
219 | // src/logical/user/user.dto.ts
220 | import { IsNotEmpty, IsNumber, IsString } from 'class-validator';
221 | import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
222 |
223 | export class LoginDTO {
224 | @ApiProperty()
225 | @IsNotEmpty({ message: '用户名不能为空' })
226 | readonly username: string;
227 | @ApiProperty()
228 | @IsNotEmpty({ message: '密码不能为空' })
229 | readonly password: string;
230 | }
231 |
232 | export class RegisterInfoDTO {
233 | ...
234 | }
235 |
236 | ```
237 |
238 | 然后在 `main.ts` 中加上 `addBearerAuth()` 方法,启用承载授权
239 |
240 | ```ts
241 | // src/main.ts
242 | import { NestFactory } from '@nestjs/core';
243 | import { AppModule } from './app.module';
244 | import * as express from 'express';
245 | import { logger } from './middleware/logger.middleware';
246 | import { TransformInterceptor } from './interceptor/transform.interceptor';
247 | import { HttpExceptionFilter } from './filter/http-exception.filter';
248 | import { AllExceptionsFilter } from './filter/any-exception.filter';
249 | import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
250 |
251 | async function bootstrap() {
252 | const app = await NestFactory.create(AppModule);
253 | ...
254 | // 配置 Swagger
255 | const options = new DocumentBuilder()
256 | .addBearerAuth() // 开启 BearerAuth 授权认证
257 | .setTitle('Nest zero to one')
258 | .setDescription('The nest-zero-to-one API description')
259 | .setVersion('1.0')
260 | .addTag('test')
261 | .build();
262 | const document = SwaggerModule.createDocument(app, options);
263 | SwaggerModule.setup('api-doc', app, document);
264 |
265 | await app.listen(3000);
266 | }
267 | bootstrap();
268 |
269 | ```
270 |
271 | 然后只需在 Controller 中添加 `@ApiBearerAuth()` 装饰器即可,顺便把登录的 DTO 也加上:
272 |
273 | ```ts
274 | // src/logical/user/user.controller.ts
275 | import { Controller, Post, Body, UseGuards, UsePipes } from '@nestjs/common';
276 | import { AuthGuard } from '@nestjs/passport';
277 | import { AuthService } from '../auth/auth.service';
278 | import { UserService } from './user.service';
279 | import { ValidationPipe } from '../../pipe/validation.pipe';
280 | import { LoginDTO, RegisterInfoDTO } from './user.dto';
281 | import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
282 |
283 | @ApiBearerAuth() // Swagger 的 JWT 验证
284 | @ApiTags('user')
285 | @Controller('user')
286 | export class UserController {
287 | constructor(
288 | private readonly authService: AuthService,
289 | private readonly usersService: UserService,
290 | ) {}
291 |
292 | // JWT 验证 - Step 1: 用户请求登录
293 | @Post('login')
294 | async login(@Body() loginParmas: LoginDTO) {
295 | // console.log('JWT验证 - Step 1: 用户请求登录');
296 | const authResult = await this.authService.validateUser(
297 | loginParmas.username,
298 | loginParmas.password,
299 | );
300 | switch (authResult.code) {
301 | case 1:
302 | return this.authService.certificate(authResult.user);
303 | case 2:
304 | return {
305 | code: 600,
306 | msg: `账号或密码不正确`,
307 | };
308 | default:
309 | return {
310 | code: 600,
311 | msg: `查无此人`,
312 | };
313 | }
314 | }
315 |
316 | @UseGuards(AuthGuard('jwt'))
317 | @UsePipes(new ValidationPipe())
318 | @Post('register')
319 | async register(@Body() body: RegisterInfoDTO) {
320 | return await this.usersService.register(body);
321 | }
322 | }
323 | ```
324 |
325 | 然后,我们去页面中登录:
326 |
327 | 
328 |
329 | 
330 |
331 | 将 `Responses body` 中的 `token` 复制出来,然后将页面拖到顶部,点击右上角那个带锁的按钮:
332 |
333 | 
334 |
335 | 将 token 复制到弹窗的输入框,点击 `Authorize`,即可授权成功:
336 |
337 | 
338 |
339 | 
340 |
341 | > 注意:这里显示的授权 `Value` 是密文,也就是,如果你复制错了,或者 token 过期了,也不会有任何提示。
342 |
343 | 现在,我们再重新请求一下注册接口:
344 |
345 | 
346 |
347 | 成功!
348 |
349 | ### 示例参数
350 |
351 | 前面登录的时候,需要手动输入用户名、密码,那么有没有可能,事先写好,这样前端来看文档的时候,直接用默认账号登录就行了呢?
352 |
353 | 我们先给 DTO 加点料:
354 |
355 | ```ts
356 | // src/logical/user/user.dto.ts
357 | import { IsNotEmpty, IsNumber, IsString } from 'class-validator';
358 | import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
359 |
360 | // @ApiExtraModels(LoginDTO)
361 | export class LoginDTO {
362 | @ApiProperty({ description: '用户名', example: 'koa2', })
363 | @IsNotEmpty({ message: '用户名不能为空' })
364 | readonly username: string;
365 | @ApiProperty({ description: '密码', example: 'a123456' })
366 | @IsNotEmpty({ message: '密码不能为空' })
367 | readonly password: string;
368 | }
369 |
370 | export class RegisterInfoDTO {
371 | ...
372 | }
373 |
374 | ```
375 |
376 | 然后,去 Controller 中引入 `ApiBody`, 并用来装饰接口,type 直接指定 `LoginDTO` 即可:
377 |
378 | ```ts
379 | // src/logical/user/user.controller.ts
380 | import { Controller, Post, Body, UseGuards, UsePipes } from '@nestjs/common';
381 | import { AuthGuard } from '@nestjs/passport';
382 | import { AuthService } from '../auth/auth.service';
383 | import { UserService } from './user.service';
384 | import { ValidationPipe } from '../../pipe/validation.pipe';
385 | import { LoginDTO, RegisterInfoDTO } from './user.dto';
386 | import { ApiTags, ApiBearerAuth, ApiBody } from '@nestjs/swagger';
387 | @ApiBearerAuth()
388 | @ApiTags('user')
389 | @Controller('user')
390 | export class UserController {
391 | constructor(private readonly authService: AuthService, private readonly usersService: UserService) {}
392 |
393 | // JWT验证 - Step 1: 用户请求登录
394 | @Post('login')
395 | @ApiBody({
396 | description: '用户登录',
397 | type: LoginDTO,
398 | })
399 | async login(@Body() loginParmas: LoginDTO) {
400 | ...
401 | }
402 |
403 | @UseGuards(AuthGuard('jwt'))
404 | @UsePipes(new ValidationPipe())
405 | @Post('register')
406 | async register(@Body() body: RegisterInfoDTO) {
407 | return await this.usersService.register(body);
408 | }
409 | }
410 |
411 | ```
412 |
413 | 保存代码,再刷新一下页面:
414 |
415 | 
416 |
417 | 并且点击 `Schema` 的时候,还能看到 DTO 详情:
418 |
419 | 
420 |
421 | 再点击 `try it out` 按钮的时候,就会自动使用默认参数了:
422 |
423 | 
424 |
425 | ## 总结
426 |
427 | 本篇介绍了如何使用 Swagger 自动生成可互动的文档。
428 |
429 | 可以看到,我们只需在写代码的时候,加一些装饰器,并配置一些属性,就可以在 Swagger UI 中生成文档,并且这个文档是根据代码,实时更新的。查看文档,只需访问链接即可,不用再传来传去了,你好我好大家好。
430 |
431 | 本篇只是抛砖引玉, Swagger UI 还有很多可配置的玩法,比如数组应该怎么写,枚举应该怎么弄,如何设置请求头等等,因为篇幅原因,就不在这里展开了。有兴趣的同学,可以自行去官网了解~
432 |
433 | > 本篇收录于[NestJS 实战教程](https://juejin.im/collection/5e893a1b6fb9a04d65a15400),更多文章敬请关注。
434 |
435 | 参考资料:
436 |
437 | [Nest 官网 - OpenAPI (Swagger)](https://docs.nestjs.com/recipes/swagger)
438 |
439 | [Swagger - OpenAPI Specification](https://swagger.io/specification/)
440 |
441 | [Swagger UI tutorial](https://idratherbewriting.com/learnapidoc/pubapis_swagger.html#make-a-request)
442 |
--------------------------------------------------------------------------------
/source/_posts/@5e3e0d2.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "用 Vue+ElementUI 搭建后台管理极简模板"
3 | description: "此篇文章为一篇说明文档,不是教你从零构建一个后台管理系统,而是基于一个实际项目,已经搭建好了一个后台管理系统的基础框架,教你如何在此基础上快速开发自己的后台管理系统,能让读者能在掌握一些基础知识的情况下,也能上手vue后台开发。只有接触项目,才能更好地理解自己所学知识的意义,触类旁通把死知识点变成活学活用的技能。"
4 | tags: ["Vue.js", "Element UI"]
5 | categories: ["前端", "Vue", "入门"]
6 | date: 2020-02-08T00:00:00.509Z
7 | photos:
8 | - https://static.tuture.co/c/%405e3e0d2.md/vue-elementui-cover.jpg
9 | ---
10 |
11 |
12 |
13 |

14 |
15 |
20 |
21 | ## 写在前面
22 |
23 | 此篇文章为一篇说明文档,不是教你从零构建一个后台管理系统,而是基于一个实际项目,已经搭建好了一个后台管理系统的基础框架,教你如何在此基础上快速开发自己的后台管理系统,能让读者能在掌握一些基础知识的情况下,也能上手vue后台开发。只有接触项目,才能更好地理解自己所学知识的意义,触类旁通把死知识点变成活学活用的技能。
24 |
25 | ## 先跑起来
26 |
27 | ```bash
28 | # 克隆项目
29 | git clone https://github.com/tuture-dev/vue-admin-template.git
30 | # 进入目录
31 | cd vue-admin-template
32 | # 安装依赖
33 | npm install --registry=https://registry.npm.taobao.org
34 | # 运行
35 | npm run dev
36 | ```
37 |
38 | 
39 |
40 | > 本文所涉及的源代码都放在了 [Github](https://github.com/tuture-dev/vue-admin-template) 上,如果您觉得我们写得还不错,希望您能给❤️这篇文章**点赞**+[Github](https://github.com/tuture-dev/vue-admin-template)仓库加星❤️哦~
41 |
42 | ## 增添侧边导航
43 | 1. 新建文件。在**src/views/** 新建一个空白的文件夹 **test**,在此目录下新建文件 **test.vue**
44 | 2. 添加路由。打开 **src/router/index.js**,此文件为该项目的后台路由配置文件。在**constantRoutes**这个数组中,添加路由的格式如下:
45 |
46 | ```js
47 | {
48 | path: '/test', //url路径
49 | component: Layout, // 此处不用动,这个全局统一的一个布局文件
50 | children: [{
51 | path: 'test', // 二级路径
52 | name: 'test',
53 | component: () => import('@/views/test/test'), // 懒加载,此处写所添加文件的路径
54 | meta: {
55 | title: '测试', icon:'plane' //配置选项可配置测试名称和图标
56 | }
57 | }]
58 | },
59 | ```
60 | 我们可以自定义图标,格式的文件,可以在[iconfont](https://www.iconfont.cn/)中下载,之后放入**src/icons/svg** 目录下即可
61 |
62 | 
63 |
64 |
65 | 对于二级导航可以按照如下的方式进行配置。
66 |
67 | 
68 |
69 | ```js
70 | {
71 | path: '/material',
72 | component: Layout,
73 | redirect: '/material/upload',
74 | meta: {
75 | title: '素材管理', //元信息,一级导航的名称
76 | icon: 'plane' // 元信息,导航图标的名称
77 | },
78 | children: [{
79 | path: 'check-template',
80 | name: 'check-template',
81 | component: () => import('@/views/material/check-template'),
82 | meta: {
83 | title: '查看模板',
84 | }
85 | },
86 | {
87 | path: 'logo',
88 | name: 'logo',
89 | component: () => import('@/views/material/check-logo'),
90 | meta: {
91 | title: '查看logo',
92 | }
93 | },
94 | {
95 | path: 'generate',
96 | name: 'generate',
97 | component: () => import('@/views/material/generate'),
98 | meta: {
99 | title: '生成素材',
100 | }
101 | },
102 | {
103 | path: 'check',
104 | name: 'check',
105 | component: () => import('@/views/material/check'),
106 | meta: {
107 | title: '查看素材',
108 | }
109 | },
110 | ]
111 | },
112 | ```
113 | 在此配置完成后,框架会自动地根据路由配置文件,生成边侧导航条目。我们所需要做的工作就是根据业务需求,编写一个个vue组件,往框架里面填充内容就OK了。
114 |
115 | ### 使用Element UI组件
116 |
117 | [Element UI](https://element.eleme.cn/#/zh-CN/component/installation)提供了很多可复用的组件,对于一般的后台应用,这些组件完全可以满足需求。如果个性化需求不高的话,我们完全可以做一名“复制粘贴”工程师又称“CV”工程师,快速开发。
118 |
119 | 对于每一个组件,其文档上都有效果示例与代码,只需选择所需组件,将其代码粘贴进我们的代码文件中,稍加修改即可。
120 |
121 | ## 网络请求
122 |
123 | 当整个框架搭建完毕以后,前端程序员最主要的工作就是发起请求,渲染数据。现在我们就来完整地走一遍这个过程。
124 |
125 | ### 基础配置
126 |
127 | 1. 配置代理。
128 |
129 | 因为跨域资源请求的问题,在开发阶段所有和后端交互的网络请求在底层由node.js代理。[相关文档](https://cli.vuejs.org/config/#devserver-proxy)
130 |
131 | 打开根目录下的**vue.config.js**文件
132 |
133 | ```js
134 | // 代理所有以‘/admin’开头的网络请求
135 | proxy: {
136 | '/admin': {
137 | target: `http://localhost:8886/`, //后台服务地址
138 | changeOrigin: true,
139 | pathRewrite: {
140 | }
141 | }
142 | }
143 | ```
144 | 2. 配置地址
145 |
146 | 生产环境与开发环境通常有不同的服务地址。编辑 **.env.development** 以及 **.env.production** 这两个文件,修改其中的 **VUE_APP_BASE_API** 配置项即可
147 |
148 | 以开发环境为例:
149 |
150 | ```js
151 | VUE_APP_BASE_API = '/admin'
152 | ```
153 |
154 | 3. 配置拦截器
155 |
156 | 打开**src/utils/request.js**,此文件封装了一个axios请求对象,该系统中的网络请求都是基于这个对象来处理的。
157 | 我们可以在网络请求发送之前和收到服务端回复之后做一些通用性的工作。比如根据服务端的状态码判断请求是否正常,若不正常给出相应的提示。
158 |
159 | ```js
160 | service.interceptors.response.use(
161 | response => {
162 | const res = response.data
163 | // 如果服务器的状态码不为200,说明请求异常,应给出错误提示。
164 | if (res.code !== 200) {
165 | Message({
166 | message: res.msg || 'Error check your token or method',
167 | type: 'error',
168 | duration: 2 * 1000
169 | })
170 | return Promise.reject(new Error(res.msg || 'Error'))
171 | } else {
172 | return res
173 | }
174 | },
175 | error => {
176 | console.log('err' + error) // for debug
177 | Message({
178 | message: error.message,
179 | type: 'error',
180 | duration: 2 * 1000
181 | })
182 | return Promise.reject(error)
183 | }
184 | )
185 | ```
186 |
187 | 4. 挂载请求对象
188 |
189 | 在**src/main.js**首先导入网络请求对象,并挂载至Vue全局对象,这样在每个组件中直接引用即可,不用要再导入。
190 |
191 | ```js
192 | import request from '@/utils/request'
193 | Vue.prototype.req = request
194 | ```
195 |
196 | ### 请求与渲染
197 | 1. 搭建一个简易node服务
198 |
199 | 仅供教程说明使用
200 |
201 | ```js
202 | let http = require('http');
203 | let querystring = require('querystring');
204 | let my_result = [{
205 | date: '2016-05-02',
206 | name: '王小虎',
207 | address: '上海市普陀区金沙江路 1518 弄'
208 | }, {
209 | date: '2016-05-04',
210 | name: '王小虎',
211 | address: '上海市普陀区金沙江路 1517 弄'
212 | }, {
213 | date: '2016-05-01',
214 | name: '王小虎',
215 | address: '上海市普陀区金沙江路 1519 弄'
216 | }, {
217 | date: '2016-05-03',
218 | name: '王小虎',
219 | address: '上海市普陀区金沙江路 1516 弄'
220 | }]
221 |
222 | let server = http.createServer((req, res) => {
223 | let post = '';
224 | req.on('data', function (chunk) {
225 | post += chunk;
226 | });
227 |
228 | req.on('end', function () {
229 | res.writeHead(200, {
230 | 'Content-Type': 'application/json; charset=utf-8'
231 | })
232 | post = querystring.parse(post);
233 | console.log('post-data:', post);
234 | if (post) {
235 | let result = {
236 | code: 200,
237 | // msg: "server error"
238 | data: my_result
239 | }
240 | res.end(JSON.stringify(result));
241 | } else {
242 | let result = {
243 | code: '0',
244 | msg: '没有接受到数据'
245 | }
246 | res.end(JSON.stringify(result));
247 | }
248 | });
249 | });
250 | server.listen(8886)
251 | //在命令行 node server.js 即可运行
252 | ```
253 |
254 | 2. 发起请求
255 |
256 | ```js
257 | this.req({
258 | url: "getInfo?id=6", // 此处写不同业务对应的url,框架会自动与baseURL拼接
259 | data: {},
260 | method: "GET"
261 | }).then(
262 | res => {
263 | // 请求成功后的处理
264 | console.log("res :", res);
265 | },
266 | err => {
267 | // 请求失败后的处理
268 | console.log("err :", err);
269 | }
270 | );
271 | ```
272 | 按照最佳实践,应该把网络请求统一抽离到单一文件,然后在每个具体的页面进行对服务端数据的处理。
273 | 比如下面的这种形式,首先创建文件**src/api/test.js**,把在**test**组件中需要用到的网络请求都写入此文件。
274 |
275 | ```js
276 | // src/api/test.js
277 | import request from '@/utils/request'
278 |
279 | export function getList(params) {
280 | return request({
281 | url: 'getTableData',
282 | method: 'get',
283 | params
284 | })
285 | }
286 |
287 |
288 | ```
289 |
290 | 在组件**test.vue**中引入请求方法
291 |
292 | ```js
293 | import { getTableData } from "@/api/test.js";
294 | ……
295 | mounted: function() {
296 | // 网络请求统一处理
297 | getTableData().then(res => {
298 | console.log("api tableData :", res);
299 | this.tableData = res.data;
300 | },err=>{
301 | console.log("err :", err);
302 | });
303 | // 网络请求直接写在文件中
304 | this.req({
305 | url: "getTableData",
306 | data: {},
307 | method: "GET"
308 | }).then(
309 | res => {
310 | console.log("tableData :", res);
311 | this.tableData = res.data;
312 | },
313 | err => {
314 | console.log("err :", err);
315 | }
316 | );
317 | },
318 | ```
319 |
320 | 3. 网络数据流
321 |
322 | 
323 |
324 | 在控制台可以看出,我们的请求地址为localhost:9528,而后台服务的的地址为localhost:8886。为啥不一样呢?我们以一流程图说明
325 |
326 | 
327 |
328 | 应用程序上线后,对于CORS跨域资源访问的问题,可以用类似的方案(Nginx反向代理)在前端解决。
329 |
330 | ### Hello Table
331 |
332 | 现在我们在**test.vue**中用Element UI所提供的 Table组件写一个表格数据展示页面。
333 |
334 | 1. 进入Element UI [Table](https://element.eleme.cn/#/zh-CN/component/table)组件的说明文档,复制粘贴对应的代码。框架对于Element UI已经进行了全局引入,所以这些组件拿来即用。如果是其他第三方的组件,还需要我们自己引入后再使用。
335 |
336 |
337 | ```html
338 |
339 |
340 |
341 |
342 |
343 |
344 |
345 | ```
346 |
347 | 2. 在组件装载时请求数据
348 |
349 | ```js
350 | mounted: function() {
351 | this.req({
352 | url: "getTableData",
353 | data: {},
354 | method: "GET"
355 | }).then(
356 | res => {
357 | console.log("tableData :", res);
358 | this.tableData = res.data // 数据渲染
359 | },
360 | err => {
361 | console.log("err :", err); // 当业务逻辑发生错误时 进行处理
362 | }
363 | );
364 | },
365 | ```
366 |
367 | 3. 实际效果
368 |
369 | 业务逻辑正常
370 |
371 | 
372 |
373 | 业务出错时,弹出服务端给的错误信息。弹出此信息是在拦截器**request.js**文件定义的,这是统一的业务逻辑错误处理,也可以在每个请求中单独处理。
374 |
375 | 
376 |
377 |
378 | ## 简易权限控制
379 |
380 | 这种权限控制方式为静态方式,有些复杂的动态权限管理不在此说明。
381 | 该框架每一次的路由跳转都会通过**router.beforeEach**检验一遍权限,我们可以在这里添加配置项。
382 | 进入文件 **src/permission.js**,以只有管理员才能进入用户管理界面为例:
383 |
384 | ```js
385 | if (to.path === '/user/user') {
386 | let id = JSON.parse(localStorage.getItem('userInfo')).id
387 | console.log(id)
388 | if (id > 2) { //id>2位普通用户,无权访问
389 | next({ path: '/task' })
390 | window.alert('permission denied')
391 | }
392 | }
393 | ```
394 |
395 | ## 结语
396 |
397 | 到此后台开发中最常用的操作已经介绍完毕,对于一些小项目已经是绰绰有余。花盆里长不出参天松,庭院里练不出千里马,项目写得多了很多东西就自然而然的通透了。一千个读者就有一千个哈姆雷特,这只是一个基础框架,在开发的过程,需要我们自己对其修改,让它成为你自己最顺手的框架。
398 |
399 | > 此项目演绎自: https://github.com/PanJiaChen/vue-admin-template.git
400 |
--------------------------------------------------------------------------------
/source/_posts/@5e60587.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "爬虫养成记——先跨进这个精彩的世界(女生定制篇)"
3 | description: "这是一套基于实战的系列教程,从最简单的爬虫程序开始,授人予渔,详细剖析程序设计的思路,完整展现爬虫是如何一步步调试到最终完成。分享关于爬虫的各种知识、技巧,旨在帮助大家认识爬虫、设计爬虫、使用爬虫最后享受爬虫带给我们在工作和生活上的各种便利。"
4 | tags: ["爬虫"]
5 | categories: ["后端", "Python", "入门"]
6 | date: 2020-03-05T00:00:00.509Z
7 | photos:
8 | - https://static.tuture.co/c/%405e60587.md/spider-1-cover.jpg
9 | ---
10 |
11 |
12 |
13 |

14 |
15 |
20 |
21 |
22 | ## 预备知识
23 | 1. 基本的python编程知识
24 | 1. 这里使用 Python3 作为开发环境
25 | 2. 同时具备基本的装包知识
26 | 2. 基本的网页编程知识
27 | 3. 初步了解HTTP协议
28 |
29 | ## 这是最简单的计算机程序
30 | 说起爬虫每个人都或多或少的听过与之相关的内容,觉得它或高深、或有趣。作为一名写代码四五年的初级码农来看,爬虫程序是计算机程序里面最简单也是最有趣的程序。**只要会上网冲浪就有写爬虫程序的天赋**。
31 |
32 | 爬虫为什么是虫呢?因为虫子的头脑比较简单,爬虫程序也是 ++“一根筋”++ ,不需要讳莫如深的数学知识,也不用设计精巧的各类算法。我们只需要用计算机能听懂的“大白话”平铺直叙就可以啦。
33 |
34 | ## 开发爬虫的基本套路
35 | 一句话总结所有爬虫程序的作用: 模拟人类上网的操作,以此来查找、下载、存储数据。
36 | 接下来我要以 [男人图](https://www.nanrentu.cc/sgtp/)这个网站为例,分享套路。
37 |
38 | ### step 1: 打开目标网址
39 |
40 | > 此处强烈推荐Chrome
41 |
42 | 首先打开这个网址:https://www.nanrentu.cc/sgtp/, 会看到以下界面
43 |
44 | 。
45 |
46 | 现在我们只是用浏览器手动打开了这个页面,接下来我们要用代码,让程序也打开这个页面。这就要分析一下浏览器是如何打开这个页面的了,请看简易流程图。
47 |
48 | 
49 |
50 | 在python中,可以使用 **requests** 这一工具包来发送HTTP请求。为了了解程序所“看到” 页面是什么样子的,我们需要把程序所得到HTML文件保存到本地,然后再用浏览器打开,就能和程序感同身受了。从而达到“人机合一”的境界。
51 |
52 | 光说不练假把式,让我们马上来新建一个 `index.py` 文件,然后在其中编写如下内容:
53 |
54 | ```Python
55 | import requests
56 | url = "https://www.nanrentu.cc/sgtp/"
57 | response = requests.get(url)
58 | if response.status_code == 200:
59 | with open("result.html",'a',encoding="utf-8") as f:
60 | f.write(response.text)
61 | ```
62 |
63 | 在浏览器打开写入的HTML文件是这样的
64 |
65 | [](https://imgchr.com/i/3IqM36)
66 |
67 | [](https://imgchr.com/i/3IqK9x)
68 |
69 | 这怎么和在浏览器中看到的不一样呢?
70 |
71 | 这个时候我就要亮出一件绝世宝贝————Chrome调试台(按F12)来给您分析一波了。
72 | 
73 |
74 | 其实我们在浏览器中看到的页面并不仅仅是HTML页面,而是css、js、html以及各种媒体资源综合在一起并有浏览器最终渲染而出页面,红框的部分,标出了在这个过程中所加载的各个资源。
75 |
76 | 当我们用程序去请求服务器时,得到仅仅是HTML页面,所以程序和我们所看到的页面就大相径庭了。不过没关系HTML是主干,抓住了主干其他的只需要顺藤摸瓜就可以了。
77 |
78 | ### step2:找到目标资源
79 |
80 | 打开这个网址以后,各位小仙女就可以各取所需咯,想体验萧亚轩的快乐嘛?那目标就是小鲜肉;馋彭于晏的那样的身子了?那肌肉帅哥就是你的菜。此外韩国欧巴,欧美型男也是应有尽有。
81 |
82 | 人类是高级生物,眼睛会自动聚焦的目标身上,但是爬虫是“一根筋”啊,它可不会自动聚焦,我们还得帮它指引道路。
83 |
84 | 写过前端页面的朋友都知道CSS样式用过各种选择器来绑定到对应的节点上,那么我们也可以通过CSS的选择器来选中我们想要的元素,从而提取信息。Chrome中已经准备了CSS选择器神器,可以生成我们想要元素的选择器。
85 |
86 | 具体过程如下:第三步为好好欣赏小哥哥们~
87 |
88 | 
89 |
90 | ### step3:解析页面
91 |
92 | 这个时候要介绍页面解析神器**pyquery**,这个工具库可以通过我们所复制的CSS选择器,在 HTML 页面中查找对应元素,并且能很便捷地提取各种属性。那么接下来我们就把这个小哥哥解析出来吧。
93 |
94 | 我们首先安装 `PyQuery` 这个包,具体可以使用 pip 包管理器安装,然后将代码修改成如下这样:
95 |
96 | ```Python
97 | import requests
98 | from pyquery import PyQuery as pq
99 | url = "https://www.nanrentu.cc/sgtp/"
100 | response = requests.get(url)
101 | if response.status_code == 200:
102 | with open("result.html",'w',encoding="utf-8") as f:
103 | f.write(response.text)
104 | # 开始解析
105 | doc = pq(response.text)
106 | # 把复制的选择器粘贴进去
107 | # 选择对应的节点
108 | imgElement = doc('body > div:nth-child(5) > div > div > div:nth-child(2) > ul > li:nth-child(3) > a > img')
109 | # 提取属性,获取图片链接
110 | imgSrc = imgElement.attr('src')
111 | # 将图片链接输出在屏幕上
112 | print(imgSrc)
113 |
114 | ```
115 |
116 | ### step4:存储目标
117 |
118 | 这么好看的小哥哥怎么能只让他在互联网上呆着呢?把他放进硬盘里的**学习资料**文件夹里才是最安全的。接下来,我们就把小哥哥放到碗里来。
119 |
120 | 下载图片的过程其实和抓取HTML页面的流程是一样的,也是利用 **requests** 发送请求从而获取到数据流再保存到本地。
121 |
122 | ```Python
123 | import requests
124 | from pyquery import PyQuery as pq
125 | url = "https://www.nanrentu.cc/sgtp/"
126 | response = requests.get(url)
127 | if response.status_code == 200:
128 | with open("result.html",'w',encoding="utf-8") as f:
129 | f.write(response.text)
130 | doc = pq(response.text)
131 | imgElement = doc('body > div:nth-child(5) > div > div > div:nth-child(2) > ul > li:nth-child(3) > a > img')
132 | imgSrc = imgElement.attr('src')
133 | print(imgSrc)
134 | # 下载图片
135 | imgResponse = requests.get(imgSrc)
136 | if imgResponse.status_code == 200:
137 | # 填写文件路径 以二进制的形式写入文件
138 | with open('学习文件/boy.jpg', 'wb') as f:
139 | f.write(imgResponse.content)
140 | f.close()
141 |
142 | ```
143 | 此时先来看看效果
144 |
145 | 
146 |
147 | ### 四步虫
148 | 至此仅仅十多行代码就完成了一个小爬虫,是不是很简单。其实爬虫的基本思路就这四步,所谓复杂的爬虫就是在这个四步的基础上不断演化而来的。爬虫最终的目的是为了获取各种资源(文本或图片),所有的操作都是以资源为核心的。
149 | 1. 打开资源
150 | 2. 定位资源
151 | 3. 解析资源
152 | 4. 下载资源
153 |
154 | ## 更多的小哥哥
155 | 通过上述步骤我们只能获取到一个小哥哥,集美们就说了,我直接右击鼠标下载也可以啊,干嘛费劲写爬虫呢?那接下来,我们就升级一波选择器,把小哥哥们装进数组,统统搞到碗里来。
156 |
157 | ### 重构代码
158 |
159 | 为了以后写代码更方便,要先进行一个简单的重构,让代码调理清晰。
160 | 1. 增加入口函数
161 | 2. 封装对于图片的操作
162 |
163 | 重构后的代码如下:
164 |
165 | ```Python
166 | import requests
167 | from pyquery import PyQuery as pq
168 |
169 | def saveImage(imgUrl,name):
170 | imgResponse = requests.get(imgUrl)
171 | fileName = "学习文件/%s.jpg" % name
172 | if imgResponse.status_code == 200:
173 | with open(fileName, 'wb') as f:
174 | f.write(imgResponse.content)
175 | f.close()
176 |
177 | def main():
178 | baseUrl = "https://www.nanrentu.cc/sgtp/"
179 | response = requests.get(baseUrl)
180 | if response.status_code == 200:
181 | with open("result.html",'w',encoding="utf-8") as f:
182 | f.write(response.text)
183 | doc = pq(response.text)
184 | imgElement = doc('body > div:nth-child(5) > div > div > div:nth-child(2) > ul > li:nth-child(3) > a > img')
185 | imgSrc = imgElement.attr('src')
186 | print(imgSrc)
187 | saveImage(imgSrc,'boy')
188 |
189 | if __name__ == "__main__":
190 | main()
191 | ```
192 |
193 | ### 升级选择器
194 |
195 | 有过前端编程经验的同学们可以看出来,Chrome自动生成的选择器指定了具体的某个子元素,所以就只选中了一个小哥哥,那么接下来我们要分析出通用的选择器,把臭弟弟们一锅端。
196 |
197 | 
198 |
199 | 多拿着鼠标点点这个调试台,一层层地看这个HTML文件的元素层级,找到其中相同重复的地方,这就是我们的突破口所在。
200 |
201 | 我们可以看出图片都在一个类名为 h-piclist 的 `
` 标签中,那么我们可写出以下的选择器 `.h-piclist > li > a > img`。这样就选中了这一页所有的图片元素。接着用一个 for 循环遍历就可以了。
202 |
203 |
204 | ```Python
205 | import requests
206 | from pyquery import PyQuery as pq
207 |
208 | # 引入UUID为图片命名
209 | import uuid
210 |
211 | def saveImage(imgUrl,name):
212 | imgResponse = requests.get(imgUrl)
213 | fileName = "学习文件/%s.jpg" % name
214 | if imgResponse.status_code == 200:
215 | with open(fileName, 'wb') as f:
216 | f.write(imgResponse.content)
217 | f.close()
218 |
219 | def main():
220 | baseUrl = "https://www.nanrentu.cc/sgtp/"
221 | response = requests.get(baseUrl)
222 | if response.status_code == 200:
223 | with open("result.html",'w',encoding="utf-8") as f:
224 | f.write(response.text)
225 | doc = pq(response.text)
226 | # 选则这一页中所有的目标图片元素
227 | imgElements = doc('.h-piclist > li > a > img').items()
228 | # 遍历这些图片元素
229 | for i in imgElements:
230 | imgSrc = i.attr('src')
231 | print(imgSrc)
232 | saveImage(imgSrc,uuid.uuid1().hex)
233 |
234 | if __name__ == "__main__":
235 | main()
236 | ```
237 |
238 | ### 无法下载的图片
239 |
240 | [](https://imgchr.com/i/3o2Ygs)
241 |
242 | 可以看出图片的连接已经全部拿到了,但是当去下载图片时却发生了一些意外,请求图片竟然没有反应。这是哪里出了问题呢?图片连接全部拿到,证明代码没毛病,把图片链接放到浏览器里正常打开,证明连接没毛病,那现在可能就是网络有毛病了。
243 | 1. 网速慢
244 | 2. 网络波动
245 | 2. 对方网站有防爬措施
246 | 3. ……
247 |
248 | 这时候因素很多,我们首先用最简单的方法来解决问题,断线重连。把笔记本WIFI重启,重新加入网络,再运行程序。
249 |
250 | 惊喜来了,臭弟弟们成功入库。
251 |
252 | 
253 |
254 | 当然啦,这种方式并不是银弹,我们需要有更多的技巧来提升爬虫程序的“演技”,我们的爬虫程序表现的越像个人,那我们获取资源的成功率就会越高。
255 |
256 | 看到这里,应该跨进爬虫世界的大门了,如果这个世界有主题曲的话那么一定是薛之谦的《演员》接下来的教程中会一遍磨砺“演技”,一遍获取更多的小哥哥。
257 |
--------------------------------------------------------------------------------
/source/_posts/@70fc9b9.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "一小时的时间,上手 Webpack"
3 | description: "为什么要用构建工具?如果你只会js+css+html,最多再加上jquery,那么当你听到构建工具这个说法是不是蒙的?这种情况下我不建议你学习webpack,建议你学下 vue 或者 react 框架,这两个框架都有自己的脚手架,所谓脚手架就是别人用构建工具帮你搭好了原始项目,你可以在不懂构建工具的情况下进行前端开发。"
4 | tags: ["Webpack"]
5 | categories: ["前端", "Webpack", "入门"]
6 | date: 2020-03-18T00:00:00.509Z
7 | photos:
8 | - https://static.tuture.co/c/%4070fc9b9.md/webpack-cover-1.jpg
9 | ---
10 |
11 |
12 |
13 |

14 |
15 |
23 |
24 |
25 | ### 序言
26 |
27 | 为什么要用构建工具?如果你只会js+css+html,最多再加上jquery,那么当你听到构建工具这个说法是不是蒙的?这种情况下我不建议你学习webpack,建议你学下 vue 或者 react 框架,这两个框架都有自己的脚手架,所谓脚手架就是别人用构建工具帮你搭好了原始项目,你可以在不懂构建工具的情况下进行前端开发。不过这就是初级前端的基本工作,给我一个环境,让我安心的写业务代码。
28 |
29 | 实际上,仅仅做上述工作也没什么不好,你可以钻研 css,研究js深度语法,甚至去不断精进算法与数据结构都是高级进阶之路。
30 |
31 | 我想说的是如果你有一天对webpack感兴趣了,ok,欢迎你来到大前端世界!webpack是一个强大且可扩展的前端构建工具,还有包括grunt、gulp等同类工具都是基于node.js为底层驱动平台实现的。
32 |
33 | 为什么需要构建或者说编译呢?因为像es6、less及sass、模板语法、vue指令及jsx在浏览器中是无法直接执行的,必须经过构建这一个操作才能保证项目运行,所以前端构建打包很重要。除了这些,前端构建还能解决一些web应用性能问题,比如:依赖打包、资源嵌入、文件压缩及hash指纹等。具体的我不再展开,总之前端构建工程化已经是趋势。
34 |
35 | 至于为什么选择webpack,因为这个工具配置非常灵活,支持插件化扩展,社区生态很丰富,官方迭代更新速度快,作为程序员,这是一个靠谱的选择。
36 |
37 | 好了,废话不多说,下面直接上干货,教你半小时上手,用webpack构建开发一个小项目。学东西要快,可以不用理解清楚每个概念,先动手,不然等你学会得要一个星期了。
38 |
39 | ### node安装
40 |
41 | 首先要保证你的电脑有node环境,node安装比较简单,直接在官网https://nodejs.org/en/下载node安装包就可以了,注意这里有两个版本,左边是稳定版本,右边是最新版本,一般用稳定版本就可以了。具体的安装过程不是本文的主要内容,网上有很多安装教程。有了node环境,后面就可以通过npm进行webpack的安装,npm是一个包管理工具,安装node就会自动安装npm。如果有必要我可以在我的公众号里也写个教程。
42 |
43 | 
44 |
45 | ### webpack安装及配置
46 |
47 | #### webpack安装
48 |
49 | 首先创建一个my\_webpack文件夹作为项目文件夹,进入当前文件夹,通过命令行工具cmd执行以下命令:
50 |
51 | ```bash
52 | npm init -y
53 | npm install webpack webpack-cli --save-dev
54 | ```
55 |
56 | 安装完了检查版本,确认安装成功
57 |
58 | ```bash
59 | ./node_modules/.bin/webpack -v
60 | ```
61 |
62 | 安装成功后,文件夹下面会有这些内容
63 |
64 | 
65 |
66 |
67 |
68 | #### webpack配置
69 |
70 | 然后在根目录创建一个空配置文件webpack.config.js,创建以下内容:
71 |
72 | ```js
73 | 'use strict'
74 | const path = require('path');
75 | module.exports = {
76 | entry: {
77 | index: './src/index.js',
78 | },
79 | output: {
80 | path: path.join(__dirname,'dist'),
81 | filename: 'index.js'
82 | },
83 | mode: 'production',
84 | }
85 | ```
86 |
87 | entry代表打包入口,output需要指定输出地址及打包出来的文件名,mode指开发环境。然后在项目根目录中创建src文件夹及dist文件夹,src文件夹里创建index.js及hellowebpack.js两个文件,最后在dist目录创建一个index.html文件,完整目录如下:
88 |
89 | 
90 |
91 | hellowebpack.js文件里输入以下内容:
92 |
93 | ```js
94 | export function hellowebpack() {
95 | return 'hellowebpack'
96 | }
97 | ```
98 |
99 | 接着在index.js中引用hellowebpack.js文件中的这个函数
100 |
101 | ```js
102 | import {hellowebpack} from './hellowebpack'
103 | document.write(hellowebpack())
104 | ```
105 |
106 | 这个时候到./node\_modules/.bin/目录下执行webpack打包命令,我们会看到dist目录下多出一个index.js文件,打开会看到压缩代码。
107 |
108 | 我们在dist目录下的indext.html文件创建如下内容,在浏览器打开此页面就能看到效果。
109 |
110 | ```html
111 |
112 |
113 |
114 |
115 |
116 |
117 | Document
118 |
119 |
120 |
121 |
122 |
123 | ```
124 |
125 | 看到这应该明白了吧,在src目录下面是开发内容,后面用 `webpack` 代码打包后,会在 dist 目录下生成一个`index.js` 文件,最后在index.html页引用,这就是webpack打包项目的基本流程。
126 |
127 | 前面在运行打包要进入 `./node\_modules/.bin/` 目录下执行 webpack打包命令,比较麻烦,这里添加一个配置就可以更方便打包。在 `package.json` 文件下的 script 节点添加一项配置 `"build": "webpack"`,然后再删掉 dist 目录,再运行 `npm run build` 就可以方便地打包了。
128 |
129 | 
130 |
131 | ### webpack入门示例
132 |
133 | #### webpack解析es6
134 |
135 | 到这一步需要掌握一个新的概念:loaders,所谓loaders就是说把原本webpack不支持加载的文件或者文件内容通过loaders进行加载解析,实现应用的目的。这里讲解es6解析,原生支持js解析,但是不能解析es6,需要babel-loader ,而babel-loader 又依赖babel。来看步骤:先安装babel-loader,
136 |
137 |
138 | ```bash
139 | npm i @babel/core @babel/preset-env babel-loader -D
140 | ```
141 |
142 | 再在根目录创建 `.babelrc` 文件,增加以下内容
143 |
144 | ```json
145 | {
146 | "presets": [
147 | "@babel/preset-env",
148 | ]
149 | }
150 | ```
151 |
152 | 接着在webpack.config.js文件下添加module属性,属性内容是一个rules集合,内容如下
153 |
154 | ```js
155 | // ...
156 | module: {
157 | rules: [
158 | {
159 | test: /.js$/,
160 | use: 'babel-loader'
161 | },
162 | ]
163 | }
164 | // ...
165 | ```
166 |
167 | rules集合的每个元素都是一个文件类型的配置信息,这里只有一个js文件,后面会讲到css、less及各种格式的图片等;test是一个正则,用来匹配文件后缀名;use表示此loader名称。
168 |
169 | 
170 |
171 | 前面例子里创建了hellowebapck.js文件,然后在index.js文件中被引用。里面用到的import是es6方法,有的浏览器并不支持es6,如果直接用webpack打包在这浏览器上是会出错的,但是刚才已经安装并配置了babel-loader,可以实现解析es6方法,babel还可以解析jsx语法。现在执行npm run build进行构建,就可以看到效果是成功的。(这个其实用一个ie浏览器就可以验证es6解析前后的效果)
172 |
173 | #### webpack加载css、less等样式文件
174 |
175 | css-loader用于加载css文件并生成commonjs对象,style-loader用于将样式通过style标签插入到head中
176 |
177 | **安装loader**
178 |
179 |
180 | ```bash
181 | npm i style-loader css-loader -D
182 | ```
183 |
184 | ```js
185 | // ...
186 | module: {
187 | rules: [
188 | {
189 | test: /.js$/,
190 | use: 'babel-loader'
191 | },
192 | {
193 | test: /.css$/,
194 | use: [
195 | 'style-loader',
196 | 'css-loader'
197 | ]
198 | },
199 | ]
200 | }
201 | // ...
202 | ```
203 |
204 | 注意,这里的解析css用到了两个loader,所以use对象里是个数组,需要格外注意到loader顺序,是先写style-loader,再写css-loader,但是执行的时候是先加载css-loader,将css解析好后再将css传递给style-loader;
205 |
206 | 接下来在项目中加一个public.css文件,在里面写入一个样式:
207 |
208 | ```css
209 | .color-text {
210 | font-size: 20px;
211 | color: red
212 | }
213 | ```
214 |
215 | 将此文件在src/index.js文件中引用,如下所示。
216 |
217 | 
218 |
219 | 我们知道此文件做为打包入口文件,最后打包后在dist目录下,我们可以直接到dist目录下的index.html文件内,添加一个div标签,加上样式名color-text,以验证样式打包及引用效果
220 |
221 | ```html
222 |
223 |
224 | text-color
225 |
226 | ```
227 |
228 | 
229 |
230 | 然后运行npm run build命令,执行成功后,在浏览器打开dist目录下index.html文件,会看到以下内容,样式文件被成功打包并发挥了作用:
231 |
232 | 
233 |
234 | 解析less文件也是一样的,直接把public.css文件改成less后缀,此时是不能解析的,需要安装less依赖,添加配置。
235 |
236 | **安装less相关依赖**
237 |
238 |
239 | ```bash
240 | npm i less less-loader -D
241 | ```
242 |
243 | **添加less文件解析配置**
244 |
245 | ```js
246 | // ...
247 | module: {
248 | rules: [
249 | {
250 | test: /.js$/,
251 | use: 'babel-loader'
252 | },
253 | {
254 | test: /.css$/,
255 | use: [
256 | 'style-loader',
257 | 'css-loader'
258 | ]
259 | },
260 | {
261 | test: /.less$/,
262 | use: [
263 | 'style-loader',
264 | 'css-loader',
265 | 'less-loader'
266 | ]
267 | },
268 | ]
269 | }
270 | // ...
271 | ```
272 |
273 | 这些步骤完成后,再运行 `npm run build` 命令进行打包,最后在浏览器查看 `dist\index.html` ,会发现效果是一样的。
274 |
275 | #### webpack加载图片
276 |
277 | 图片在webpack中的打包步骤跟上面类似,只不过loader不同。
278 |
279 | **安装file-loader**
280 |
281 | 执行以下命令,安装file-loader依赖
282 |
283 | ```bash
284 | npm i file-loader -D
285 | ```
286 |
287 | 然后在webpack.config.js配置文件 `module` 节点下添加解析配置内容:
288 |
289 |
290 | ```js
291 | {
292 | test: /.(jpg|png|gif|jpeg)$/,
293 | use: 'file-loader'
294 | }
295 | ```
296 |
297 | 随便找一张图片放在src目录下,在同级目录的public.css文件中加上背景图片样式,输入内容如下:
298 |
299 |
300 | ```css
301 | .color-text {
302 | font-size: 20px;
303 | color: red;
304 | height: 500px;
305 | background: url(beautiful.jpg) no-repeat;
306 | }
307 | ```
308 |
309 | 然后运行npm run build命令进行构建,最后执行时并没有图片展示,但是我们在dist目录下发现了刚才打包过来的图片,如图所示。这就尴尬了,虽然图片是打包过来了,问题是我每次还在拷贝复制一下名称再引用,这很不科学。
310 |
311 | 
312 |
313 | 有没有更好的办法加载图片呢?答案是肯定的!看步骤:
314 |
315 | **安装url-loader**
316 |
317 | ```bash
318 | npm i url-loader -D
319 | ```
320 |
321 | url-loader直接内置了file-loader,是对它的再封装,在配置文件里可以直接去掉file,用url替换。
322 |
323 | 在webpack.config.js文件添加配置内容,注意limit是指图片大小上限,单位是字节,如果图片大小小于这个值,就会被打包为base64格式,否则就仍是图片。由于这个图片有120K,我这里设置为160000,差不多是150多K了。
324 |
325 | ```js
326 | {
327 | test: /.(jpg|png|gif|jpeg)$/,
328 | use: [{
329 | loader:'url-loader',
330 | options: {
331 | limit:160000,
332 | name: 'imgs/[name].[hash].[ext]'
333 | }
334 | }]
335 | }
336 | ```
337 |
338 | 执行npm run build查看效果,发现成功了,再看dist目录下的index.js文件,发现内容多了很多,其实就是多了这张图片的base64数据。
339 |
340 | 
341 |
342 | 好了,这篇文章就写到这里,如果你按我的步骤来,一定可以轻松体验入门。当然,要真正达到应用水平,还要继续深入学习,webpack的内容还有很多,比如如何加载vue指令或者jsx语法,如何打包组件等,后面我会继续带来入门教程。
343 |
--------------------------------------------------------------------------------
/source/_posts/@auY0siFek.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 'Nest.js 从零到壹系列(四):使用中间件、拦截器、过滤器打造日志系统'
3 | description: '上一篇介绍了如何使用 JWT 进行单点登录,接下来,要完善一下后端项目的一些基础功能。'
4 | tags: ['Nest.js']
5 | categories: ['后端', 'Node.js', '进阶']
6 | date: 2020-05-12T00:03:00.509Z
7 | photos:
8 | - https://static.tuture.co/c/%40auY0siFek/4.jpg
9 | ---
10 |
11 |
12 |
13 |

14 |
15 |
20 |
21 |
22 | ## 前言
23 |
24 | 上一篇介绍了如何使用 JWT 进行单点登录,接下来,要完善一下后端项目的一些基础功能。
25 |
26 | 首先,一个良好的服务端,应该有较完善的日志收集功能,这样才能在生产环境发生异常时,能够从日志中复盘,找出 Bug 所在。
27 |
28 | 其次,要针对项目中抛出的异常进行归类,并将信息反映在接口或日志中。
29 |
30 | 最后,请求接口的参数也应该被记录,以便统计分析(主要用于大数据和恶意攻击分析)。
31 |
32 | [GitHub 项目地址](https://github.com/SephirothKid/nest-zero-to-one),欢迎各位大佬 Star。
33 |
34 | ## 一、日志系统
35 |
36 | 这里使用的是 `log4js`,前身是 `log4j`,如果有写过 Java 的大佬应该不会陌生。
37 |
38 | 已经有大佬总结了 log4js 的用法,就不在赘述了:
39 |
40 | [《Node.js 之 log4js 完全讲解》](https://juejin.im/post/57b962af7db2a200542a0fb3)
41 |
42 | ### 1. 配置
43 |
44 | 先安装依赖包
45 |
46 | ```ts
47 | $ yarn add log4js stacktrace-js -S
48 |
49 | ```
50 |
51 | 在 config 目录下新建一个文件 `log4js.ts`,用于编写配置文件:
52 |
53 | ```ts
54 | // config/log4js.ts
55 |
56 | import * as path from 'path';
57 | const baseLogPath = path.resolve(__dirname, '../../logs'); // 日志要写入哪个目录
58 |
59 | const log4jsConfig = {
60 | appenders: {
61 | console: {
62 | type: 'console', // 会打印到控制台
63 | },
64 | access: {
65 | type: 'dateFile', // 会写入文件,并按照日期分类
66 | filename: `${baseLogPath}/access/access.log`, // 日志文件名,会命名为:access.20200320.log
67 | alwaysIncludePattern: true,
68 | pattern: 'yyyyMMdd',
69 | daysToKeep: 60,
70 | numBackups: 3,
71 | category: 'http',
72 | keepFileExt: true, // 是否保留文件后缀
73 | },
74 | app: {
75 | type: 'dateFile',
76 | filename: `${baseLogPath}/app-out/app.log`,
77 | alwaysIncludePattern: true,
78 | layout: {
79 | type: 'pattern',
80 | pattern:
81 | '{"date":"%d","level":"%p","category":"%c","host":"%h","pid":"%z","data":\'%m\'}',
82 | },
83 | // 日志文件按日期(天)切割
84 | pattern: 'yyyyMMdd',
85 | daysToKeep: 60,
86 | // maxLogSize: 10485760,
87 | numBackups: 3,
88 | keepFileExt: true,
89 | },
90 | errorFile: {
91 | type: 'dateFile',
92 | filename: `${baseLogPath}/errors/error.log`,
93 | alwaysIncludePattern: true,
94 | layout: {
95 | type: 'pattern',
96 | pattern:
97 | '{"date":"%d","level":"%p","category":"%c","host":"%h","pid":"%z","data":\'%m\'}',
98 | },
99 | // 日志文件按日期(天)切割
100 | pattern: 'yyyyMMdd',
101 | daysToKeep: 60,
102 | // maxLogSize: 10485760,
103 | numBackups: 3,
104 | keepFileExt: true,
105 | },
106 | errors: {
107 | type: 'logLevelFilter',
108 | level: 'ERROR',
109 | appender: 'errorFile',
110 | },
111 | },
112 | categories: {
113 | default: {
114 | appenders: ['console', 'app', 'errors'],
115 | level: 'DEBUG',
116 | },
117 | info: { appenders: ['console', 'app', 'errors'], level: 'info' },
118 | access: { appenders: ['console', 'app', 'errors'], level: 'info' },
119 | http: { appenders: ['access'], level: 'DEBUG' },
120 | },
121 | pm2: true, // 使用 pm2 来管理项目时,打开
122 | pm2InstanceVar: 'INSTANCE_ID', // 会根据 pm2 分配的 id 进行区分,以免各进程在写日志时造成冲突
123 | };
124 |
125 | export default log4jsConfig;
126 | ```
127 |
128 | 上面贴出了我的配置,并标注了一些简单的注释,请配合 [《Node.js 之 log4js 完全讲解》](https://juejin.im/post/57b962af7db2a200542a0fb3) 一起食用。
129 |
130 | ### 2. 实例化
131 |
132 | 有了配置,就可以着手写 log4js 的实例以及一些工具函数了。
133 |
134 | 在 `src/utils` 下新建 `log4js.ts`:
135 |
136 | ```ts
137 | // src/utils/log4js.ts
138 | import * as Path from 'path';
139 | import * as Log4js from 'log4js';
140 | import * as Util from 'util';
141 | import * as Moment from 'moment'; // 处理时间的工具
142 | import * as StackTrace from 'stacktrace-js';
143 | import Chalk from 'chalk';
144 | import config from '../../config/log4js';
145 |
146 | // 日志级别
147 | export enum LoggerLevel {
148 | ALL = 'ALL',
149 | MARK = 'MARK',
150 | TRACE = 'TRACE',
151 | DEBUG = 'DEBUG',
152 | INFO = 'INFO',
153 | WARN = 'WARN',
154 | ERROR = 'ERROR',
155 | FATAL = 'FATAL',
156 | OFF = 'OFF',
157 | }
158 |
159 | // 内容跟踪类
160 | export class ContextTrace {
161 | constructor(
162 | public readonly context: string,
163 | public readonly path?: string,
164 | public readonly lineNumber?: number,
165 | public readonly columnNumber?: number,
166 | ) {}
167 | }
168 |
169 | Log4js.addLayout('Awesome-nest', (logConfig: any) => {
170 | return (logEvent: Log4js.LoggingEvent): string => {
171 | let moduleName: string = '';
172 | let position: string = '';
173 |
174 | // 日志组装
175 | const messageList: string[] = [];
176 | logEvent.data.forEach((value: any) => {
177 | if (value instanceof ContextTrace) {
178 | moduleName = value.context;
179 | // 显示触发日志的坐标(行,列)
180 | if (value.lineNumber && value.columnNumber) {
181 | position = `${value.lineNumber}, ${value.columnNumber}`;
182 | }
183 | return;
184 | }
185 |
186 | if (typeof value !== 'string') {
187 | value = Util.inspect(value, false, 3, true);
188 | }
189 |
190 | messageList.push(value);
191 | });
192 |
193 | // 日志组成部分
194 | const messageOutput: string = messageList.join(' ');
195 | const positionOutput: string = position ? ` [${position}]` : '';
196 | const typeOutput: string = `[${
197 | logConfig.type
198 | }] ${logEvent.pid.toString()} - `;
199 | const dateOutput: string = `${Moment(logEvent.startTime).format(
200 | 'YYYY-MM-DD HH:mm:ss',
201 | )}`;
202 | const moduleOutput: string = moduleName
203 | ? `[${moduleName}] `
204 | : '[LoggerService] ';
205 | let levelOutput: string = `[${logEvent.level}] ${messageOutput}`;
206 |
207 | // 根据日志级别,用不同颜色区分
208 | switch (logEvent.level.toString()) {
209 | case LoggerLevel.DEBUG:
210 | levelOutput = Chalk.green(levelOutput);
211 | break;
212 | case LoggerLevel.INFO:
213 | levelOutput = Chalk.cyan(levelOutput);
214 | break;
215 | case LoggerLevel.WARN:
216 | levelOutput = Chalk.yellow(levelOutput);
217 | break;
218 | case LoggerLevel.ERROR:
219 | levelOutput = Chalk.red(levelOutput);
220 | break;
221 | case LoggerLevel.FATAL:
222 | levelOutput = Chalk.hex('#DD4C35')(levelOutput);
223 | break;
224 | default:
225 | levelOutput = Chalk.grey(levelOutput);
226 | break;
227 | }
228 |
229 | return `${Chalk.green(typeOutput)}${dateOutput} ${Chalk.yellow(
230 | moduleOutput,
231 | )}${levelOutput}${positionOutput}`;
232 | };
233 | });
234 |
235 | // 注入配置
236 | Log4js.configure(config);
237 |
238 | // 实例化
239 | const logger = Log4js.getLogger();
240 | logger.level = LoggerLevel.TRACE;
241 |
242 | export class Logger {
243 | static trace(...args) {
244 | logger.trace(Logger.getStackTrace(), ...args);
245 | }
246 |
247 | static debug(...args) {
248 | logger.debug(Logger.getStackTrace(), ...args);
249 | }
250 |
251 | static log(...args) {
252 | logger.info(Logger.getStackTrace(), ...args);
253 | }
254 |
255 | static info(...args) {
256 | logger.info(Logger.getStackTrace(), ...args);
257 | }
258 |
259 | static warn(...args) {
260 | logger.warn(Logger.getStackTrace(), ...args);
261 | }
262 |
263 | static warning(...args) {
264 | logger.warn(Logger.getStackTrace(), ...args);
265 | }
266 |
267 | static error(...args) {
268 | logger.error(Logger.getStackTrace(), ...args);
269 | }
270 |
271 | static fatal(...args) {
272 | logger.fatal(Logger.getStackTrace(), ...args);
273 | }
274 |
275 | static access(...args) {
276 | const loggerCustom = Log4js.getLogger('http');
277 | loggerCustom.info(Logger.getStackTrace(), ...args);
278 | }
279 |
280 | // 日志追踪,可以追溯到哪个文件、第几行第几列
281 | static getStackTrace(deep: number = 2): string {
282 | const stackList: StackTrace.StackFrame[] = StackTrace.getSync();
283 | const stackInfo: StackTrace.StackFrame = stackList[deep];
284 |
285 | const lineNumber: number = stackInfo.lineNumber;
286 | const columnNumber: number = stackInfo.columnNumber;
287 | const fileName: string = stackInfo.fileName;
288 | const basename: string = Path.basename(fileName);
289 | return `${basename}(line: ${lineNumber}, column: ${columnNumber}): \n`;
290 | }
291 | }
292 | ```
293 |
294 | 上面贴出了我实例化 log4js 的过程,主要是处理日志的组成部分(包含了时间、类型,调用文件以及调用的坐标),还可以根据日志的不同级别,在控制台中用不同的颜色显示。
295 |
296 | 这个文件,不但可以单独调用,也可以做成中间件使用。
297 |
298 | ### 3. 制作中间件
299 |
300 | 我们希望每次用户请求接口的时候,自动记录请求的路由、IP、参数等信息,如果每个路由都写,那就太傻了,所以需要借助中间件来实现。
301 |
302 | Nest 中间件实际上等价于 express 中间件。
303 |
304 | 中间件函数可以执行以下任务:
305 |
306 | - 执行任何代码;
307 | - 对请求和响应对象进行更改;
308 | - 结束请求-响应周期;
309 | - 调用堆栈中的下一个中间件函数;
310 | - 如果当前的中间件函数没有【结束请求】或【响应周期】, 它必须调用 `next()` 将控制传递给下一个中间件函数。否则,请求将被挂起;
311 |
312 | 执行下列命令,创建中间件文件:
313 |
314 | ```ts
315 | $ nest g middleware logger middleware
316 |
317 | ```
318 |
319 | 然后,`src` 目录下,就多出了一个 `middleware` 的文件夹,里面的 `logger.middleware.ts` 就是接下来的主角,Nest 预设的中间件模板长这样:
320 |
321 | ```ts
322 | // src/middleware/logger.middleware.ts
323 | import { Injectable, NestMiddleware } from '@nestjs/common';
324 |
325 | @Injectable()
326 | export class LoggerMiddleware implements NestMiddleware {
327 | use(req: any, res: any, next: () => void) {
328 | next();
329 | }
330 | }
331 | ```
332 |
333 | 这里只是实现了 `NestMiddleware` 接口,它接收 3 个参数:
334 |
335 | - req:即 Request,请求信息;
336 | - res:即 Response ,响应信息;
337 | - next:将控制传递到下一个中间件,写过 Vue、Koa 的应该不会陌生;
338 |
339 | 接下来,我们将日志功能写入中间件:
340 |
341 | ```ts
342 | // src/middleware/logger.middleware.ts
343 | import { Injectable, NestMiddleware } from '@nestjs/common';
344 | import { Request, Response } from 'express';
345 | import { Logger } from '../utils/log4js';
346 |
347 | @Injectable()
348 | export class LoggerMiddleware implements NestMiddleware {
349 | use(req: Request, res: Response, next: () => void) {
350 | const code = res.statusCode; // 响应状态码
351 | next();
352 | // 组装日志信息
353 | const logFormat = `Method: ${req.method} \n Request original url: ${req.originalUrl} \n IP: ${req.ip} \n Status code: ${code} \n`;
354 | // 根据状态码,进行日志类型区分
355 | if (code >= 500) {
356 | Logger.error(logFormat);
357 | } else if (code >= 400) {
358 | Logger.warn(logFormat);
359 | } else {
360 | Logger.access(logFormat);
361 | Logger.log(logFormat);
362 | }
363 | }
364 | }
365 | ```
366 |
367 | 同时,Nest 也支持【函数式中间件】,我们将上面的功能用函数式实现一下:
368 |
369 | ```ts
370 | // src/middleware/logger.middleware.ts
371 | import { Injectable, NestMiddleware } from '@nestjs/common';
372 | import { Request, Response } from 'express';
373 | import { Logger } from '../utils/log4js';
374 |
375 | @Injectable()
376 | export class LoggerMiddleware implements NestMiddleware {
377 | ...
378 | }
379 |
380 | // 函数式中间件
381 | export function logger(req: Request, res: Response, next: () => any) {
382 | const code = res.statusCode; // 响应状态码
383 | next();
384 | // 组装日志信息
385 | const logFormat = ` >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
386 | Request original url: ${req.originalUrl}
387 | Method: ${req.method}
388 | IP: ${req.ip}
389 | Status code: ${code}
390 | Parmas: ${JSON.stringify(req.params)}
391 | Query: ${JSON.stringify(req.query)}
392 | Body: ${JSON.stringify(req.body)} \n >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
393 | `;
394 | // 根据状态码,进行日志类型区分
395 | if (code >= 500) {
396 | Logger.error(logFormat);
397 | } else if (code >= 400) {
398 | Logger.warn(logFormat);
399 | } else {
400 | Logger.access(logFormat);
401 | Logger.log(logFormat);
402 | }
403 | }
404 |
405 | ```
406 |
407 | 上面的日志格式进行了一些改动,主要是为了方便查看。
408 |
409 | 至于使用 Nest 提供的还是函数式中间件,可以视需求决定。当然,Nest 原生的中间件高级玩法会更多一些。
410 |
411 | ### 4. 应用中间件
412 |
413 | 做好中间件后,我们只需要将中间件引入 main.ts 中就好了:
414 |
415 | ```ts
416 | // src/main.ts
417 | import { NestFactory } from '@nestjs/core';
418 | import { AppModule } from './app.module';
419 |
420 | import { logger } from './middleware/logger.middleware';
421 |
422 | async function bootstrap() {
423 | const app = await NestFactory.create(AppModule);
424 | // 监听所有的请求路由,并打印日志
425 | app.use(logger);
426 | app.setGlobalPrefix('nest-zero-to-one');
427 | await app.listen(3000);
428 | }
429 | bootstrap();
430 | ```
431 |
432 | 保存代码后,就会发现,项目目录下就多了几个文件:
433 |
434 | 
435 |
436 | 这就是之前 `config/log4js.ts` 中配置的成果
437 |
438 | 接下来,我们试着请求一下登录接口:
439 |
440 | 
441 |
442 | 发现虽然是打印了,但是没有请求参数信息。
443 |
444 | 于是,我们还要做一部操作,将请求参数处理一下:
445 |
446 | ```ts
447 | // src/main.ts
448 | import { NestFactory } from '@nestjs/core';
449 | import { AppModule } from './app.module';
450 | import * as express from 'express';
451 | import { logger } from './middleware/logger.middleware';
452 |
453 | async function bootstrap() {
454 | const app = await NestFactory.create(AppModule);
455 | app.use(express.json()); // For parsing application/json
456 | app.use(express.urlencoded({ extended: true })); // For parsing application/x-www-form-urlencoded
457 | // 监听所有的请求路由,并打印日志
458 | app.use(logger);
459 | app.setGlobalPrefix('nest-zero-to-one');
460 | await app.listen(3000);
461 | }
462 | bootstrap();
463 | ```
464 |
465 | 再请求一次,发现参数已经出来了:
466 |
467 | 
468 |
469 | > 上面的打印信息,IP 为 `::1` 是因为我所有的东西都跑在本地,正常情况下,会打印对方的 IP 的。
470 |
471 | 再去看看 `logs/` 文件夹下:
472 |
473 | 
474 |
475 | 上图可以看到日志已经写入文件了。
476 |
477 | ### 5. 初探拦截器
478 |
479 | 前面已经示范了怎么打印入参,但是光有入参信息,没有出参信息肯定不行的,不然怎么定位 Bug 呢。
480 |
481 | Nest 提供了一种叫做 `Interceptors`(拦截器) 的东东,你可以理解为关卡,除非遇到关羽这样的可以过五关斩六将,否则所有的参数都会经过这里进行处理,正所谓雁过拔毛。
482 |
483 | 详细的使用方法会在后面的教程进行讲解,这里只是先大致介绍一下怎么使用:
484 |
485 | 执行下列指令,创建 `transform`文件
486 |
487 | ```bash
488 | $ nest g interceptor transform interceptor
489 |
490 | ```
491 |
492 | 然后编写出参打印逻辑,intercept 接受两个参数,当前的上下文和传递函数,这里还使用了 `pipe`(管道),用于传递响应数据:
493 |
494 | ```ts
495 | // src/interceptor/transform.interceptor.ts
496 | import {
497 | CallHandler,
498 | ExecutionContext,
499 | Injectable,
500 | NestInterceptor,
501 | } from '@nestjs/common';
502 | import { Observable } from 'rxjs';
503 | import { map } from 'rxjs/operators';
504 | import { Logger } from '../utils/log4js';
505 |
506 | @Injectable()
507 | export class TransformInterceptor implements NestInterceptor {
508 | intercept(context: ExecutionContext, next: CallHandler): Observable {
509 | const req = context.getArgByIndex(1).req;
510 | return next.handle().pipe(
511 | map((data) => {
512 | const logFormat = ` <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
513 | Request original url: ${req.originalUrl}
514 | Method: ${req.method}
515 | IP: ${req.ip}
516 | User: ${JSON.stringify(req.user)}
517 | Response data:\n ${JSON.stringify(data.data)}
518 | <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<`;
519 | Logger.info(logFormat);
520 | Logger.access(logFormat);
521 | return data;
522 | }),
523 | );
524 | }
525 | }
526 | ```
527 |
528 | 保存文件,然后在 `main.ts` 中引入,使用 `useGlobalInterceptors` 调用全局拦截器:
529 |
530 | ```ts
531 | import { NestFactory } from '@nestjs/core';
532 | import { AppModule } from './app.module';
533 | import * as express from 'express';
534 | import { logger } from './middleware/logger.middleware';
535 | import { TransformInterceptor } from './interceptor/transform.interceptor';
536 |
537 | async function bootstrap() {
538 | const app = await NestFactory.create(AppModule);
539 | app.use(express.json()); // For parsing application/json
540 | app.use(express.urlencoded({ extended: true })); // For parsing application/x-www-form-urlencoded
541 | // 监听所有的请求路由,并打印日志
542 | app.use(logger);
543 | // 使用全局拦截器打印出参
544 | app.useGlobalInterceptors(new TransformInterceptor());
545 | app.setGlobalPrefix('nest-zero-to-one');
546 | await app.listen(3000);
547 | }
548 | bootstrap();
549 | ```
550 |
551 | 我们再试一次登录接口:
552 |
553 | 
554 |
555 | 可以看到,出参的日志已经出来了,User 为 `undefiend` 是因为登录接口没有使用 JWT 守卫,若路由加了 `@UseGuards(AuthGuard('jwt'))`,则会把用户信息绑定在 req 上,具体操作可回顾上一篇教程。
556 |
557 | ## 二、异常处理
558 |
559 | 在开发的过程中,难免会写出各式各样的“八阿哥”,不然程序员就要失业了。一个富有爱心的程序员应该在输出代码的同时创造出 3 个岗位(手动狗头)。
560 |
561 | 
562 |
563 | 回归正题,光有入参出参日志还不够,异常的捕获和抛出也需要记录。
564 |
565 | 接下来,我们先故意写错语法,看看控制台打印什么:
566 |
567 | 
568 |
569 | 如图,只会记录入参以及控制台默认的报错信息,而默认的报错信息,是不会写入日志文件的。
570 |
571 | 再看看请求的返回数据:
572 |
573 | 
574 |
575 | 如图,这里只会看到 "Internal server error",其他什么信息都没有。
576 |
577 | 这样就会有隐患了,用户在使用过程中报错了,但是日志没有记录报错的原因,就无法统计影响范围,如果是简单的报错还好,如果涉及数据库各种事务或者并发问题,就很难追踪定位了,总不能一直看着控制台吧。
578 |
579 | 因此,我们需要捕获代码中未捕获的异常,并记录日志到 `logs/errors` 里,方便登录线上服务器,对错误日志进行筛选、排查。
580 |
581 | ### 1. 初探过滤器
582 |
583 | Nest 不光提供了拦截器,也提供了过滤器,就代码结构而言,和拦截器很相似。
584 |
585 | 内置的异常层负责处理整个应用程序中的所有抛出的异常。当捕获到未处理的异常时,最终用户将收到友好的响应。
586 |
587 | 我们先新建一个 `http-exception.filter` 试试:
588 |
589 | ```bash
590 | $ nest g filter http-exception filter
591 |
592 | ```
593 |
594 | 打开文件,默认代码长这样:
595 |
596 | ```ts
597 | // src/filter/http-exception.filter.ts
598 | import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common';
599 |
600 | @Catch()
601 | export class HttpExceptionFilter implements ExceptionFilter {
602 | catch(exception: T, host: ArgumentsHost) {}
603 | }
604 | ```
605 |
606 | 可以看到,和拦截器的结构大同小异,也是接收 2 个参数,只不过用了 `@Catch()` 来修饰。
607 |
608 | ### 2. HTTP 错误的捕获
609 |
610 | Nest 提供了一个内置的 HttpException 类,它从 @nestjs/common 包中导入。对于典型的基于 HTTP REST/GraphQL API 的应用程序,最佳实践是在发生某些错误情况时发送标准 HTTP 响应对象。
611 |
612 | HttpException 构造函数有两个必要的参数来决定响应:
613 |
614 | - response 参数定义 JSON 响应体。它可以是 string 或 object,如下所述。
615 | - status 参数定义 HTTP 状态代码。
616 |
617 | 默认情况下,JSON 响应主体包含两个属性:
618 |
619 | - statusCode:默认为 status 参数中提供的 HTTP 状态代码
620 | - message:基于状态的 HTTP 错误的简短描述
621 |
622 | 我们先来编写捕获打印的逻辑:
623 |
624 | ```ts
625 | // src/filter/http-exception.filter.ts
626 | import {
627 | ExceptionFilter,
628 | Catch,
629 | ArgumentsHost,
630 | HttpException,
631 | } from '@nestjs/common';
632 | import { Request, Response } from 'express';
633 | import { Logger } from '../utils/log4js';
634 |
635 | @Catch(HttpException)
636 | export class HttpExceptionFilter implements ExceptionFilter {
637 | catch(exception: HttpException, host: ArgumentsHost) {
638 | const ctx = host.switchToHttp();
639 | const response = ctx.getResponse();
640 | const request = ctx.getRequest();
641 | const status = exception.getStatus();
642 |
643 | const logFormat = ` <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
644 | Request original url: ${request.originalUrl}
645 | Method: ${request.method}
646 | IP: ${request.ip}
647 | Status code: ${status}
648 | Response: ${exception.toString()} \n <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
649 | `;
650 | Logger.info(logFormat);
651 | response.status(status).json({
652 | statusCode: status,
653 | error: exception.message,
654 | msg: `${status >= 500 ? 'Service Error' : 'Client Error'}`,
655 | });
656 | }
657 | }
658 | ```
659 |
660 | 上面代码表示如何捕获 HTTP 异常,并组装成更友好的信息返回给用户。
661 |
662 | 我们测试一下,先把注册接口的 Token 去掉,请求:
663 |
664 | 
665 |
666 | 上图是还没有加过滤器的请求结果。
667 |
668 | 我们在 main.ts 中引入 `http-exception`:
669 |
670 | ```ts
671 | // src/main.ts
672 | import { NestFactory } from '@nestjs/core';
673 | import { AppModule } from './app.module';
674 | import * as express from 'express';
675 | import { logger } from './middleware/logger.middleware';
676 | import { TransformInterceptor } from './interceptor/transform.interceptor';
677 | import { HttpExceptionFilter } from './filter/http-exception.filter';
678 |
679 | async function bootstrap() {
680 | const app = await NestFactory.create(AppModule);
681 | app.use(express.json()); // For parsing application/json
682 | app.use(express.urlencoded({ extended: true })); // For parsing application/x-www-form-urlencoded
683 | // 监听所有的请求路由,并打印日志
684 | app.use(logger);
685 | // 使用拦截器打印出参
686 | app.useGlobalInterceptors(new TransformInterceptor());
687 | app.setGlobalPrefix('nest-zero-to-one');
688 | // 过滤处理 HTTP 异常
689 | app.useGlobalFilters(new HttpExceptionFilter());
690 | await app.listen(3000);
691 | }
692 | bootstrap();
693 | ```
694 |
695 | 使用全局过滤器 `useGlobalFilters` 调用 `http-exception`,再请求:
696 |
697 | 
698 |
699 | 再看控制台打印:
700 |
701 | 
702 |
703 | 
704 |
705 | 如此一来,就可以看到未带 Token 请求的结果了,具体信息的组装,可以根据个人喜好进行修改。
706 |
707 | ### 3. 内置 HTTP 异常
708 |
709 | 为了减少样板代码,Nest 提供了一系列继承自核心异常 HttpException 的可用异常。所有这些都可以在 @nestjs/common 包中找到:
710 |
711 | - BadRequestException
712 | - UnauthorizedException
713 | - NotFoundException
714 | - ForbiddenException
715 | - NotAcceptableException
716 | - RequestTimeoutException
717 | - ConflictException
718 | - GoneException
719 | - PayloadTooLargeException
720 | - UnsupportedMediaTypeException
721 | - UnprocessableException
722 | - InternalServerErrorException
723 | - NotImplementedException
724 | - BadGatewayException
725 | - ServiceUnavailableException
726 | - GatewayTimeoutException
727 |
728 | 结合这些,可以自定义抛出的异常类型,比如后面的教程说到权限管理的时候,就可以抛出 `ForbiddenException` 异常了。
729 |
730 | ### 4. 其他错误的捕获
731 |
732 | 除了 HTTP 相关的异常,还可以捕获项目中出现的所有异常,我们新建 `any-exception.filter`:
733 |
734 | ```bash
735 | $ nest g filter any-exception filter
736 |
737 | ```
738 |
739 | 一样的套路:
740 |
741 | ```ts
742 | // src/filter/any-exception.filter.ts
743 | /**
744 | * 捕获所有异常
745 | */
746 | import {
747 | ExceptionFilter,
748 | Catch,
749 | ArgumentsHost,
750 | HttpException,
751 | HttpStatus,
752 | } from '@nestjs/common';
753 | import { Logger } from '../utils/log4js';
754 |
755 | @Catch()
756 | export class AllExceptionsFilter implements ExceptionFilter {
757 | catch(exception: unknown, host: ArgumentsHost) {
758 | const ctx = host.switchToHttp();
759 | const response = ctx.getResponse();
760 | const request = ctx.getRequest();
761 |
762 | const status =
763 | exception instanceof HttpException
764 | ? exception.getStatus()
765 | : HttpStatus.INTERNAL_SERVER_ERROR;
766 |
767 | const logFormat = ` <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
768 | Request original url: ${request.originalUrl}
769 | Method: ${request.method}
770 | IP: ${request.ip}
771 | Status code: ${status}
772 | Response: ${exception} \n <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
773 | `;
774 | Logger.error(logFormat);
775 | response.status(status).json({
776 | statusCode: status,
777 | msg: `Service Error: ${exception}`,
778 | });
779 | }
780 | }
781 | ```
782 |
783 | 和 `http-exception` 的唯一区别就是 `exception` 的类型是 `unknown`
784 |
785 | 我们将 any-exception 引入 main.ts:
786 |
787 | ```ts
788 | // src/main.ts
789 | import { NestFactory } from '@nestjs/core';
790 | import { AppModule } from './app.module';
791 | import * as express from 'express';
792 | import { logger } from './middleware/logger.middleware';
793 | import { TransformInterceptor } from './interceptor/transform.interceptor';
794 | import { HttpExceptionFilter } from './filter/http-exception.filter';
795 | import { AllExceptionsFilter } from './filter/any-exception.filter';
796 |
797 | async function bootstrap() {
798 | const app = await NestFactory.create(AppModule);
799 | app.use(express.json()); // For parsing application/json
800 | app.use(express.urlencoded({ extended: true })); // For parsing application/x-www-form-urlencoded
801 | // 监听所有的请求路由,并打印日志
802 | app.use(logger);
803 | // 使用拦截器打印出参
804 | app.useGlobalInterceptors(new TransformInterceptor());
805 | app.setGlobalPrefix('nest-zero-to-one');
806 | app.useGlobalFilters(new AllExceptionsFilter());
807 | app.useGlobalFilters(new HttpExceptionFilter());
808 | await app.listen(3000);
809 | }
810 | bootstrap();
811 | ```
812 |
813 | > 注意:AllExceptionsFilter 要在 HttpExceptionFilter 的上面,否则 HttpExceptionFilter 就不生效了,全被 AllExceptionsFilter 捕获了。
814 |
815 | 然后,我们带上 Token (为了跳过 401 报错)再请求一次:
816 |
817 | 
818 |
819 | 再看看控制台:
820 |
821 | 
822 |
823 | 已经有了明显的区别,再看看 errors.log,也写进了日志中:
824 |
825 | 
826 |
827 | 如此一来,代码中未捕获的错误也能从日志中查到了。
828 |
829 | ## 总结
830 |
831 | 本篇介绍了如何使用 log4js 来管理日志,制作中间件和拦截器对入参出参进行记录,以及使用过滤器对异常进行处理。
832 |
833 | 文中日志的打印格式可以按照自己喜好进行排版,不一定局限于此。
834 |
835 | 良好的日志管理能帮我们快速排查 Bug,减少加班,不做资本家的奴隶,把有限的精力投入到无限的可能上。
836 |
837 | 
838 |
839 | 下一篇将介绍如何使用 DTO 对参数进行验证,解脱各种 if - else。
840 |
841 | > 本篇收录于[NestJS 实战教程](https://juejin.im/collection/5e893a1b6fb9a04d65a15400),更多文章敬请关注。
842 |
843 | 参考资料:
844 |
845 | [Nest.js 官方文档](https://docs.nestjs.com/)
846 |
847 | [Nest.js 中文文档](https://docs.nestjs.cn/)
848 |
849 | [《Node.js 之 log4js 完全讲解》](https://juejin.im/post/57b962af7db2a200542a0fb3)
850 |
851 | `
852 |
--------------------------------------------------------------------------------
/source/_posts/@d5269af.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "你所需要的跨域问题的全套解决方案都在这里啦!(前后端都有)"
3 | description: "随着RESTful架构风格成为主流,以及Vue.js、React.js和Angular.js这三大前端框架的日益强大,越来越多的开发者开始由传统的MVC架构转向基于前后端分离这一基础架构来构建自己的系统,将前端页面和后端服务分别部署在不同的域名之下。在此过程中一个重要的问题就是跨域资源访问的问题,通常由于同域安全策略浏览器会拦截JavaScript脚本的跨域网络请求,这也就造成了系统上线时前端无法访问后端资源这一问题。笔者将结合自身开发经验,对这一问题产生的原因以及相应的解决方案,给出详细介绍。"
4 | date: 2020-03-30T00:00:00.509Z
5 | photos:
6 | - https://static.tuture.co/c/%40d5269af.md/cors-cover.jpg
7 | ---
8 |
9 |
10 |
11 |

12 |
13 |
18 |
19 |
20 | ## 导论
21 |
22 | 随着RESTful架构风格成为主流,以及Vue.js、React.js和Angular.js这三大前端框架的日益强大,越来越多的开发者开始由传统的MVC架构转向基于前后端分离这一基础架构来构建自己的系统,将前端页面和后端服务分别部署在不同的域名之下。在此过程中一个重要的问题就是跨域资源访问的问题,通常由于同域安全策略浏览器会拦截JavaScript脚本的跨域网络请求,这也就造成了系统上线时前端无法访问后端资源这一问题。笔者将结合自身开发经验,对这一问题产生的原因以及相应的解决方案,给出详细介绍。
23 |
24 | ## 问题原因
25 | ### 同源策略
26 | 同源策略,它是由Netscape提出的一个著名的安全策略。现在所有支持JavaScript 的浏览器都会使用这个策略。**所谓同源是指:协议、域名、端口相同**。
27 |
28 | 举个例子:
29 |
30 | 
31 |
32 | 一个浏览器的两个tab页中分别打开来百度和谷歌的页面,当浏览器的百度tab页执行一个脚本的时候会检查这个脚本是属于哪个页面的,即检查是否同源,只有和百度同源的脚本才会被执行。如果非同源,那么在请求数据时,浏览器会在控制台中报一个异常,提示拒绝访问。**同源策略是浏览器的行为**,是为了保护本地数据不被JavaScript代码获取回来的数据污染,因此拦截的是客户端发出的请求回来的数据接收,即请求发送了,服务器响应了,但是无法被浏览器接收。
33 |
34 | ### 现象分析
35 | 在前端开发阶段,一些框架的脚手架工具会使用webpack-dev-serve来代理数据请求,其本质上是一个基于node.js的网页服务器,所以感受不到跨域资源访问的限制。
36 |
37 | 
38 |
39 | 当网站上线后,网页上很多资源都是要通过发送AJAX请求向服务器索要资源,但是在前后端分离的系统架构中,前端页面和后端服务往往不会部署在同一域名之下。比如用户通过浏览器访问 *http://www.test001.com* 这一地址,来到了系统首页,此时浏览器从网站服务器中只取回了基本的HTML页面以及CSS样式表文件和JavaScript脚本。系统首页的其他内容,比如轮播图、文章列表等,需要利用JavaScript脚本程序,向地址为 *http://www.test002.com* 的后端应用服务器发送请求来获取信息。此时由于浏览器的同源策略,该请求会被浏览器所拦截,这就造成了前后端数据不通这一结果。
40 |
41 | 
42 |
43 | ## 解决方案
44 | ### 前端解决方案
45 | #### 反向代理
46 | 因为由于浏览器的同源策略,JavaScript脚本程序只能向同一域名下的服务器发送网络请求,那么可以通过网页服务器转发这一网络请求到相应的后端服务器,获取相关数据,然后网页服务器再把这一数据返回给浏览器。这一过程称之为反向代理。
47 |
48 | 假设用户通过地址*http://www.test001.com*访问到了系统首页,该系统首页中所加载的JavaScript脚步程序**本应该要**发送AJAX请求到*http://www.test002.com/api/articleList*这一地址,来获取首页文章列表信息。此时应该**改成**向*http://www.test001.com/api/articleList*这一与之同源的地址发送数据请求。该系统的网页服务器会收到此请求,然后**代替**JavaScript脚本程序向*http://www.test002.com/api/articleList*这一地址请求数据,获取数据后将之返回给浏览器。此时JavaScript脚本程序就通过网页服务器这一**桥梁**成功获取到了后端应用服务器上的数据。
49 |
50 | 
51 |
52 | 若服务器采用了[宝塔面板](https://www.bt.cn/)这一管理软件,可以直接通过其提供的可视化界面进行反向代理的设置。对于一些新手而言,直接面对命令行进行各种操作,不够直观且难度较高,此时采用一些可视化的服务器管理软件是一个不错的选择。
53 |
54 | 
55 |
56 | 若是喜欢用vim 直接在命令行里修改的同学可以参考这篇[博客](https://blog.csdn.net/linlin_0904/article/details/89633150)
57 |
58 | 这个解决方案是不是有些眼熟呢?
59 |
60 | #### JSONP跨域
61 |
62 | 浏览器的同源策略对JavaScript脚本向不同域的服务器请求数据进行了限制,但是没有对HTML中的\
77 |
78 | ```
79 | 在这里值得注意的是,因为请求数据的接口地址是写在了\