admin管理员组文章数量:1122850
【笔记
目录 | 课程名 | 备注 |
---|---|---|
入门必学 | nodejs入门到企业web开发中的应用 | |
框架与工具 | node.js+koa2+mysql打造前后端分离精品项目《旧岛》 | |
项目实战 |
20190317-20200720:《imooc-nodejs入门到企业web开发中的应用》 |
第01章 课程内容介绍
01-01 导学
一、课程安排
1、nodejs核心api
2、静态资源服务器 && http协议
3、项目代码构建、打包
4、api 单元测试&ui测试
5、headless爬虫
6、回顾总结
二、nodejs创建项目
1、cd Desktop
2、express mockData
3、cd mockData
4、npm i
5、npm start
(aSuncat) 三、node热更新
1、npm install --sava-dev nodemon
2、项目根目录新增文件nodemon.json
{"restartable": "rs","ignore": [".git",".svn","node_modules/**/node_modules"],"verbose": true,"execMap": {"js": "node --harmony"},"watch": ["routes/"],"env": {"NODE_ENV": "development"},"ext": "js json"
}
watch: routes/指的是监控routes文件夹下的所有文件
ext:js json指的是监控后缀为js json的文件
3、package.json
scripts添加
"hotStart": "nodemon"
4、浏览器输入localhost:3000就能访问到网页了。
四、如果是创建最简单的服务
1、创建一个js,server.js
var http = require('http');http.createServer((req, res) => {console.log(`request come:${req.url}`)res.end('123');
}).listen('8080');console.log('请求成功了');
2、node server.js
3、浏览器输入localhost:8080,就能看到结果了。
第02章 nodejs是什么,为什么偏爱Nodejs
02-01 nodejs是什么
一、nodejs
1、非阻塞I/O
input output,计算机输入输出,键盘,显示机,打印机,都是I/O设备,读写磁盘,网络操作
2、阻塞:I/O时进程休眠等待I/O完成后进行下一步
3、非阻塞:I/O时函数立即返回,进程不等待I/O完成
4、cpu:计算机1秒钟执行30亿条指令
二、事件驱动
1、I/O等异步操作结束后的通知
2、观察者模式
02-02 nodejs究竟好在哪里
一、nodejs好处/nodejs适用场景
1、处理高并发、I/O密集的web场景性能优势明显
二、cpu密集 vs I/O密集
cpu密集:压缩、解压、加密、解密(计算、逻辑判断)
I/O密集:文件操作、网络操作、数据库
三、web常见场景
1、静态资源获取
2、数据库操作
3、渲染页面:读取模板文件,生成html
四、高并发应对之道(高并发:单位时间内访问量特别大)
1、增加机器数
2、增加每台机器的cpu数-多核
五、进程
1、进程:是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位
2、多进程:启动多个进程,多个进程可以一块执行多个任务。
3、单进程,一个cpu只开一个进程,一个进程只开一个线程。
4、单线程只是针对主线程,I/O操作系统底层多线程调度。
5、单线程并不是单进程
六、主进程 event loop
七、单线程
1、单线程的好处:
(1)多线程占用内存高
(2)多线程间切换使得CPU开销大
(3)多线程由内存同步开销
(4)编写单线程程序简单
(5)线程安全
2、单线程的劣势:
(1)CPU密集型任务占用CPU时间长
(2)无法利用CPU的多核
(3)单线程抛出异常使得程序停止
八、常用场景
1、web server
2、本地代码构建
3、使用工具开发
第03章 环境&调试
03-01 commonjs1
一、环境
1、安装nodejs
nodejs.org进入官网
二、commonjs
1、commonjs是nodejs使用的模块规范
2、没有window这个全局对象,有window
3、process
4、每个文件是一个模块,有自己的作用域
5、在模块内部module变量代表模块本身
6、module.exports属性代表模块对外接口
三、node
1、 node test.js // 运行test.js
2、node --inspect-brk test.js 调试test.js,在chrome浏览器的调试工具f12的sources的test.js中就能看到调试信息
(function(exports, require, module, _filename, __dirname) { // module代表模块本身console.log('this is a test');
}
);
3、chrome打开inspects
03-02 commonjs2
一、require规则
1、/表示绝对路径,./表示相对于当前文件的路径。
2、支持js、json、node扩展名,不写依次尝试
3、不写路径则认为是build-in模块或者各级node_modules内的第三方模块
二、require特性
1、module被加载的时候执行,加载后缓存(当我们加载一个模块时,这个模块里的内容都会被执行)
2、一旦出现某个模块被循环加载(A require B, B require A),就只输出已经执行的部分,还有未执行的部分不会输出。
03-04 引用第三方模块
一、异步I/O,执行之后立即返回,不在乎结果是什么
二、buffer:缓冲,二进制数据处理
三、fs是用来操作二进制流的
四、npm install… 的时候,会创建node_modules,把相关依赖下载到node_mobules;
五、npm root -g 得到全局的node_modules依赖
03-05 imports与exports的区别
一、exports是module.exports的快捷方式
exports.test = 1; // 等同于module.exports.test = 1;
二、不能改变exports的指向
exports = { // exports修改了指向,不再生效了a: 1,b: 2,test: 3
}
module.exports = { // 应该写成module.exports,这是exports的指向仍是module,所以会生效a: 1,b: 2,test: 3
}
03-06 global变量
一、global自带属性方法
1、CommonJS
2、Buffer、 process、 console
3、timer:setTimeout()、setInterval()、clearTimeout()、clearInterval()、setImmediate
二、global特性
1、直接在模块中写的是局部变量。
2、模块中写了global会变成全局变量
testStr = '1'; // 局部变量
global.testStr = '2'; // 全局变量
03-07 progress进程
一、process 进程
uncaughtException
:异常的捕获
1、process属性
const {argv, argv0, execArgv, execPath} = process; // argv:启动process时所有的参数; argv0:保存了argv第一个值的引用(不常用);execArgv:调用node传入的特殊参数(写在文件名之前的参数是不会进入argv的统计的); execPath:调用它的脚本的路径
argv.forEach(item => { // node安装的路径,当前文件路径console.log(item);
});
2、process方法
process.cwd():当前process执行的路径
// 1
setImmediate(() => {console.log('setImmediate')}) // 等下一个事件队列, 与时间无关。放在下一队列的队首。比setTimeout()慢
// 2
process.nextTick(() => { // 当前事件队列中的东西执行完了,再执行它。比setImmediate执行得早。放在当前队列的队尾。比setTimeout() 快console.log('nextTick');
})
一般情况下用setImmediate()
如果nextTick()循环调用,后面正常的异步就没办法执行了。
03-08 debug1
一、调试
1、inspector
进入官网,node-docs-inspector,可以看到api
控制台执行node --inspect-brk test.js
2、vscode:编辑器
第04章 nodejs基础api
04-01 path1
一、path:处理和路径相关的一切
path.join()也会调用nomalize
path.resolve():根据相对路径,得到绝对路径
const {basename, dirname, extname} = require('path'); // basename:文件名;dirname:所在路径;extname:扩展名
process.env.PATH可以看到path
二、nodejs.cn:nodejs中文网站
04-02 path2
一、path
1、__dirname、__filename总是返回文件的绝对路径
2、process.cwd()总是返回执行node命令所在文件夹
二、./
1、在require方法中总是相对当前文件所在文件夹
2、在其他地方和process.cwd()一样,相对Node启动文件夹
04-03 buffer1
一、常用的:file和网络
二、buffer三个要点
1、buffer用于处理二进制数据流
2、实例类似整数数组,大小固定
3、c++代码在v8堆外分配物理内存
三、nodejs跑的代码并不纯粹是用javascript跑的,还有一部分是c++
四、buffer是一个全局对象
04-04 buffer2
一、buffer静态属性方法(类的方法)
Buffer.byteLength();
Buffer.isBuffer();
Buffer.concat();
二、实例常用的属性和方法
buf.legnth
buf.toString() // 默认是utf-8,buf.toString('base64')
buf.fill()
buf.equals()
buf.indexOf()
buf.copy()
04-05 buffer3
一、中文乱码问题
const StringDecoder = require('string_decode');
const decoder = new StringDecoder('utf8');const buf = Buffer.from('中文字符串'); // 3个字符表达一个汉字
for (let i = 0; i < buf.legnth; i += 5) {const b = Buffer.allocUnsafe(5);buf.copy(b, 0, i);// console.log(b.toString()); // 乱码console.log(decoder.write(b)); // 正常
}
04-06 event1
一、events
const EventEmitter = require('events');class MyEmitter extends EventEmitter {}const myEmitter = new MyEmitter();
myEmitter.on('error', (err, time) => {console.log('触发事件', err);console.log('时间戳:', time);
});
myEmitter.emit('error', new Error('test error!'), Date.now());
04-07 event2
一、removeListener('error', fn1);
removeAllListeners('error');
04-08 fs1
一、
const fs = require('fs');
const content = Buffer.from('this is a test.');fs.writeFile('./test', content, err => {if (err) throw err;console.log('done');
});
二、可以用stat判断文件是否存在
const fs = require('fs');
fs.stat(filePath, (err, stats) => {if (err) {res.statusCode = 404;res.setHeader('Content-Type', 'text/plain');res.end(`${filePath} is not a directory or file`);return;}if (stats.isFile()) {res.statusCode = 200;res.setHeader('Content-Type', 'text/plain');fs.createReadStream(filePath).pipe(res);} else if (stats.isDirectory()) {fs.readdir(filePath, (err,files) => {res.statusCode = 200;res.setHeader(res.join(','));}) }
})
三、建议大家不要用同步的方式去判断,如果在高并发情况下,会阻塞其他用户。
四、
const fs = require('fs');
fs.rename('./text', 'test.txt', err => {if(err) throw err;console.log('done!');
});
五、watch可以watch任何内容,watchFile只能watch一个文件
六、recursive美 /rɪˈkɜːrsɪv/ adj, 递归的,循环的
第06章 项目实战-静态资源服务器
06-02 静态资源服务器 02
一、node热更新,supervisor,只能监听js文件的变化,不能监听tpl文件的变化。
npm i -g supervisor
06-05 静态资源服务 05
一、模板引擎 handlebars
1、handlebars官网文档:/
2、安装
npm i handlebars
3、使用
const Handlebars = require('handlebars');
二、不同目录下启动,得到的相对路径是不同的。
eg:有个文件,路径为~/Users/imooc/test/routes/node01.js,这个文件里写了这段代码
const source = fs.readFileSync('../template/dir.tpl');
其他它想调用的文件路径是~/Users/imooc/test/template/dir.tpl
以下列举不同的两种情况
1、终端: cd ~/imooc/test
node node01.js
此时路径为 ~/Users/imooc/template/dir.tpl
2、终端:cd ~/imooc/test/routes
node node01.js
此时路径为~/Users/imooc/test/template/dir.tpl
三、解决方案:path,不同目录下启动,用相对路径获取文件
const path = require('path');
const tplpath = path.join(__dirname, '../template/dir.tpl');
const source = fs.readFileSync(tplPath);
06-06 静态资源服务器 06
一、compile接收的字符串,readFileSync读出来的是buffer,以下两种处理方式,推荐使用2,因为2更快。
1、utf-8
const source = fs.readFileSync(tplPath, 'utf-8'); // 读出来的是buffer,加上'utf-8',把buffer转换成字符串
const template = Handlebars.compile(source);
2、toString()
const source = fs.readFileSync(tplPath);
const template = Handlebars.compile(source.toString())
06-07 静态资源服务器 07
一、config/defaultConfig.js
module.exports = {root: process.cwd(); // 项目根目录hostname: '127.0.0.1',port: 9527
}
二、如果使用的是require,可以放心地使用相对路径。
三、Content-Type
06-08 压缩文件
一、
compress: /\.(html|js|css|md)/
二、压缩的文件,浏览器支持的压缩方式request的accept-encoding
,告诉浏览器我们使用的压缩方式
Accept-Encoding
Content-Encoding
module.exports = (rs, req, res) => {const acceptEncoding = req.headers['accept-encoding'];if (!acceptEncoding || !acceptEncoding.match(/\b(gzip|deflate)\b/)) {return rs; } else if () {res.setHeader('Content-Encoding', 'gzip');}
}
06-09 range范围请求
一、range: bytes=[start]-[end]
Accept-Ranges: bytes
Content-Range: bytes start-ent/total
二、
module.export = (totalSize, req, res) => {const sizes = range.match(/bytes=(\d*)-(\d*)/);
}
06-10 缓存
一、缓存
二、缓存header
1、Exprires, Cache-Control
2、If-Modified-Since / Last-modified
3、If-None-Match / Etag
三、
if (expires) {res.setHeader('Expires', (new Date(Date.now() + maxAge * 1000).toUTCString()))
}
if (cacheControl) {res.setHeader('Cache-Control', `public, max-age=${maxAge}`);
}
if (lastModified) {res.setHeader('Last-Modified', stats.mtime.toUTCString());
}
if (etag) {res.setHeader('Etag', `${stats.size}-${stats.mtime}`);
}
06-11 --cli
一、处理process.args,如-p --port是同种东西,就直接处理成相同的
1、commander
2、yargs
npm i yargs
二、使用
const yargs = require('yargs');
const argv = yargs.usage('anywhere [options]').option('p', {alias: 'port',describe: '端口号',default: 9527})
chmod +x bin/anydoor 修改执行权限
ll bin/anydoor
三、
1、代码提交代码到gitlab
2、版本号x.y.z
1.2.*和~1.2.0是一样的
06-13 --cli
一、自动打开浏览器
openUrl.js
const { exec } = require('child_process');module.exports = url => {switch (process.platform) {case 'darwin': // macexec(`open ${url}`);break;case 'win32': // windowsexec(`start ${url}`);break;}
}
app.js
const openUrl = require('./helper/openUrl');
server.iisten(port, hostname, () => {const addr = `http://${hostname}:${port}`;console.info(addr);openUrl(addr);
});
二、包
1、上传包
(1)登录npm官方站点,注册账号
(2)终端执行
npm login
npm publish
3、使用
npm i -g anydoor
anydoor -p 9906 -d /usr
第07章 本地构建
一、nodejs在前端的用法
1、web server
2、前端开发周边工具
二、npm install gulp -g -D
-D 代表–save-dev
三、如果别人的gulp装在全局,自己把别人的项目download下来后,
在gulpfile.js文件中引入gulpimport gulp from 'gulp'
terminal运行程序时报错:command not found: gulp
1、原因:自己本地没有全局的gulp,只是在本项目中安装了gulp
2、解决方案:
(1)全局安装
(2)方法2:利用npm 的scripts解决
定义一个任务gulp
pacakge.json中添加
"scripts": {"gulp": "gulp",
}
node_modules中的.bin目录暴露了自己安装的包的”可执行的引用“,类似于帮我们手工写了快捷方式。
四、*匹配任意个字符
?匹配一个字符
[…]匹配范围内字符
![pattern1|pattern2] 匹配取反
?(pattern1|pattern2) 匹配0或1个
+(pattern1|pattern2) 匹配1或多个
(a|b|c) 匹配任意个
@(pattern|pat|pat?erN) 匹配特定的一个
** 任意层级匹配
07-02 gulp2
一、rm -rf buld && gulp
二、npm包:del可以删除一个或多个文件
const del = require('del');
gulp.task('clean', () => {del.sync('build');
})
三、css自动添加前缀
gulp-autoprefixer
四、css压缩
gulp-clean-css
五、watch就会占用终端,不会退出
1、
gulpfile.js
gulp.task('watch', () => {const watcher = gulp.watch('src/**/*', ['default'])watch.on('change', event => {})
})
2、启动
(1)方法1:npm run gulp watch
(2)方法2:
package.json
"gulp": "gulp",
"gulp-watch": "gulp watch",
07-03 babel
一、babel
babel is a javaScript compiler
二、
npm install --save-dev babel-presets-env
三、"presets": ["env"]
指的是可以把es6/ es7的语法转换成ecmascript2015
四、babel的plugins可以找到除了"presets": "env"外的其他presets
五、babel在webpack中的使用
1、
loader: "babel-loader",
options: {}
可以在webpack.config.js中使用options,也可以用.bablerc文件
07-06 webpack-module
一、css单独拎出来,extract-text-webpack-plugin
引用import、实例化new、应用plugins
二、externals,
防止将某些 import 的包(package)打包到 bundle 中,而是在运行时(runtime)再去从外部获取这些扩展依赖(external dependencies)。
externals: {jquery: 'jQuery'
}
这样就剥离了那些不需要改动的依赖模块.
三、如果不是单页面应用,多个文件都需要用到react, react-dom,则使用vendor,能复用
entry: {index: '',vendor: ['react', 'react-dom']
},
plugins: [new webpack.optimize.CommonsChunkPlugin({name: ['vendor', 'runtime']})
]
四、运行时,webpack的模块系统,能单独拎出来。
五、tree-shaking,没有用到的方法不被打包进去。比如,lodash中只用到了部分方法,那么没有被使用的方法function就不要打包进去。
plugins = [new UglifyJSPlugin()
]
.babelrc需要配置"modules": false
{"presets": [["env", {"modules": false}], "react"],
}
第08章
播放不出来,下次再试试
第09章 UI测试常用工具
09-01 UI测试1
一、react组件ui测试,jest
1、和mocha语法非常像
2、.babelrc需要配置
二、dom测试
Enzyme或react的testutils。
三、sinon,测试方法被调用几次
四、selenium-webdriver
1、自动测试
2、可以通过api来模拟用户行为
第10章 案例项目-headless爬虫
10-01 爬虫与反爬虫简介
一、爬虫
1、定义:按照一定规则自动抓取网络信息的程序
二、反爬虫技巧
1、User-Agent:爬虫不是正常的User-Agent, 不过可以伪造
Referer,
验证码:12306
2、单位时间访问次数、访问量
3、关键信息图片混淆
4、异步加载
三、superAgent, 帮我们发送请求,得到html
cheerio,把html的内容转换成像jquery一样的对象
四、cheerio缺点
1、无法绕过反爬虫
五、GoogleChrome/ puppeteer
文档:
1、 npm i puppeteer
六、gitignore
1、.gitignore目录一没了,就会报错
所以不能写screenshot/
,得写成screenshot/*.png
如果目录是空的,git就不会上传。
2、为了让screenshot文件夹不是空的,在文件夹中放一个空的文件,文件名叫.gitkeeper
10-03 Puppeteer API
一、puppeteer的evaluate很像javascript的el
二、
const browser = await puppeteer.lauch({headless: false}); // default is true
默认打开浏览器
10-05 爬虫代码实现
一、获取页面的元素
page.$(selector)
page.$$(selector)
二、获取页面元素属性
page $eval(selector, pageFunction[, ...args])
10-06 爬虫代码实现2
const { promisify } = require('util');
const writeFile = promisify(fs.writeFile);
const urlToImg = promisify((url, dir, callback) => {mod.get(url, res => {res.pipe(fs.createWriteStream(file)).on('finish', () => {callback()console.log(file)})})
})const base64ToImg = async function (base64Str, dir) {// data:image/jpeg;base64,/asdasdaconst matches = base64Str.match(/^data:(.+?);base64,(.+$/))const ext = matches[1].split('/')[1]const file = path.join(dir, `${Date.now()}.${ext}`)await writeFile(file, matches[2], 'base64')console.log(file)
}
二、sleep
page.waitFor()
第11章 课程小结
一、nodejs关键技术
1、Stream
2、动态Web framework(express/ koa)
3、child_process & cluster (单线程)
二、深入学习
1、through2
2、express、koa、egg
3、ssr & 同构
4、nodejs源码
20200722-2020:《node.js+koa2+mysql打造前后端分离精品项目《旧岛》》 |
课程地址:.html
第01章 【导学】从0到1手把手教你用nodejs KOA2打造超好用的web框架
01-01 纯正商业级应用Node.js KOA2 开发微信小程序服务号-导学
一、知识点
egg.js think.js
校验器LinValidator
全局异常处理
自动路由注册
koa核心机制
为什么要有洋葱模型
中间件到底怎么用
查找类(class)的属性和方法
异步变成模型
async await
sequelize与MySQL
二、nodejs能力
脱离浏览器运行js
nodejs stream(前端工程化基础)
服务端api
作为中间层
三、CTO
需要设计整个公司架构
需要从全局考虑问题
需要掌握公司最重要的资产:数据(谁掌握数据,谁才有话语权)
四、
双层结构:前端+服务器
三层结构:前端+后端+服务端(返回的数据很纯粹,不关乎业务)
01-02 异步、javascript特性与nodejs
一、服务端语言
1、Python Flask
2、Java SprintBoot
3、NodeJS KOA
二、javascript是基于原型链设计的,越来越向工程化(OO)对齐,用面向对象的方式编程、构建代码。
三、flask django:同步
nodejs是强制用异步的。
四、koa: async await
第02章 【深入理解KOA】koa2的那点事儿与异步编程模型
02-01 软件与环境
一、框架、库
node.js
npm
koa
二、软件、工具
MySQL(xampp)
微信开发者工具
visual studio code
postman
navicat:数据可视化工具
nodemon(自动重启工具,自动重启服务器node server)、pm2(部署程序的工具)
三、长期支持版本 LTS
LTS= Long Term Support 长期支持
四、nvm可以管理不同的node版本
02-02 node一小步,前端一大步
一、vs code 修改语言
1、command + shift + p,打开vs code的命令面板
configue display language,安装自己想要的语言
02-03 koa的精简特性与二次开发必要性
一、数据库
1、悲观锁 乐观锁 事务 脏读 幻读
二、,cms框架,vue+koa
02-04 模块加载、es、ts、babel
一、导入包/模块
1、commonJS
2、ES6
import…from
3、AMD
二、javascript新特性制定:
1、es10, babel
2、TS Typescript
(1)比较好的javascript新特性
(2)有特性约束,大型项目中比较好维护
三、koa的内核也是用typescript编写的
02-05 koa的中间件
一、koa 应用程序对象,特点是在对象上有很多中间件
const Koa = require('koa')
const app = new Koa()
app.listen(3001)
二、只有应用程序被阻塞了,才能监听到从前端发过来的http请求
三、将函数注册到对象上,函数就成了中间件
四、发送请求
浏览器
小程序
五、调用服务,可以浏览器地址栏输入以下的任何一种
(1)localhost:3000
(2)127.0.0.1:3000
六、ctx, next是koa内部机制自动传入的参数,不需要开发者管理
app.use(async (ctx, next) => {console.log(1)await next()console.log(2)
})
app.use(async (ctx, next) => {console.log(3)await next()console.log(4)
})
打印 1 3 4 2
02-06 洋葱模型
一、洋葱模型
02-08 深入理解async 和await
一、next()的调用结果一定是个promise
const a = next()
const a = await next()
app.use(async (ctx, next) => {const a = await next() // 和async配合使用,a是一个promise,即Promise('hello world')const b = next() // b得到的是next()直接返回的值,即’hello world‘
})
app.use((ctx, next) => {return 'hello world'
})
二、await会阻塞线程,会求值(会计算表达式,包括promise,返回最终值)
三、异步
1、对资源的操作
2、读文件
3、发送http
(1)需安装包:库(axios)
4、操作数据库
(1)需安装包:sequelize
四、总结:需安装的包
axios
sequelize
koa-router
五
app.use((ctx, next) => {const axios = require('axios')const res = axios.get('')
})
六、如果有async,函数的任何返回值都会被包装成promise
七、面试题:为什么很多中间件函数前面要加async
1、函数用了await,如果不加async,await会报错
八、async, await最早出现在c#语言,异步终极解决方案
02-09 为什么一定要保证洋葱模型
一、怎样保证中间件按洋葱模型执行
使用async, await
二、每个中间件执行的时候,都会被注入ctx
用ctx传值的时候,必须要保证中间件按洋葱模型执行
app.use(async (ctx, next) => {await next()const r1 = ctx.r3
})
app.use(async (ctx, next) => {const axios = require('axios')const res = await axios.get('')ctx.r3 = resawait next()
})
三、nodejs写的项目,最大并发量比其他框架好
第03章 【让KOA更加好用】 路由系统的改造
03-01 路由
一、koa自己会将数据返回成application/json
二、koa返回数据,ctx.body
三、koa-router使用的3步
1、Router对象实例化
2、router实例对象编写一系列路由函数
3、app.use()注册
四、get post put delete
03-03 多Router拆分
一、版本号携带策略
1、路径 /v1/classic/latest
2、查询参数 /classic/latest?version=v1
3、header
二、修改代码是存在风险的。
开闭原则:编程的时候,对代码的修改是关闭的,对代码的扩展是开放的。
三、对每个版本的api,都写一个路由
四、一旦产生项目循环引用,nodejs是不会报错的。
03-04 nodemon自动重启
一、npm i -g nodemon
二、使用
terminal输入nodemon app.js,实现自动重启
三、全局安装的包是不会出现在项目的package.json里的
四、如果不是全局安装的nodemon,是不能直接使用nodemon app.js的,需要npx nodemon app.js,也可以在packag.json的scripts里面使用
scripts: {'start:dev': 'nodemon app.js'
}
03-05 vscode+nodemon调试
一、vscode, 点爬虫,点lauch,点击添加配置,得到launch.json
launch.json里面自定义启动方式
{"configurations": [{"console": "integratedTerminal","internalConsoleOptions": "neverOpen","name": "nodemon","program": "${workspaceFolder}/app.js","request": "launch","restart": true,"runtimeExecutable": "nodemon","skipFiles": ["<node_internals>/**"],"type": "pwa-node"},{"type": "node","request": "launch","name": "Launch Program","skipFiles": ["<node_internals>/**"],"program": "${workspaceFolder}/index.js"},{"type": "node","request": "launch","name": "当前文件","program": "${file}"}]
}
03-06 requireDirectory实现路由自动加载
一、路由自动加载的方式/原理
1、app文件夹中寻找所有的路由模块,自动require进来
2、通过循环的方式,注册在app上
二、require-directory
1、安装
npm i require-directory
2、使用
const requireDirectory = require('require-directory')
const modules = requireDirectory(module, './api/v1')
for (let i in modules) {}
3、Lin CMS可以让module.exports = router和module.exports = { router }都能被处理
四、core/init.js中的requireDirectory(module, '../api/v1', { visit: whenLoadModule })
,’…/api/v1’,随着core文件夹放的位置不一样,里面的路径需要随着改变。解决方法
1、path config, 让开发者可以自己配置
2、用绝对路径
const apiDirectory = `${process.cwd()}/app/api`
requireDirectory(module, apiDirectory, {visit: whenLoadModule
})
五、如果想得到process.cwd()
的值,
调试,当前行process.cwd()
打上断点,选中process.cwd()
,右键,选择调试求值(evaluate in debug console),就会在控制台打印出结果
第04章 【深入浅出讲异常】异步异常与全局异常处理
04-01 参数获取与LinValidator校验器
一、常见传参方式,参数获取方式
1、url路径
/api/book/detail/:id
(1)获取
ctx.params
2、search
/api/book/detail?id=1
(1)获取
ctx.request.query
3、header
(1)获取
ctx.request.header
4、body
只有post才能在body中传参数
(1)获取
用到npm库koa-bodyparser
,中间件
二、浏览器url只能发送get请求,不能发送post请求
三、python 接口参数校验:WTSForms
04-02 异常处理与异常链
一、函数设计
1、判断出异常
return false
return null
2、throw new Error
二、提高写代码的质量:《代码大全2》
三、1/0 ,Infinity
php, python, java, c#都会报错, 0不能为分母
四、ReferenceError是node内置错误
五、如果return false,或者return null会丢失error信息
六、中间件的本质还是一个函数
七、机制:可以监听到任何异常
八、前后端联调的效率不高的很大一个原因是:后端返回的错误信息不够具体
04-03 异步异常处理方案
一、方法内部try{}catch{}是对同步方法有效果,如果方法内有异步方法,有时候try…catch是执行不到的
04-04 全局异常处理中间件编写
一、promise不是通过throw new error返回错误,而是通过reject返回错误信息
二、unhandled promise: promise有未处理的异常,可以用await,因为await能监听到这个异常。
三、全局异常处理,2步
1、全局监听到错误
app.js
const catchError = require('./middlewares/exception')
app.use(catchError)
2、监听到错误之后,输出一段有意义的提示信息
middlewares/exception.js
const catchError = async (ctx, next) => {try {await next()} catch (error) {ctx.body = '服务器有点问题,你等一下'}
}module.exports = catchError
四、AOP 面向切面编程
04-05 已知错误与未知错误
一、error信息给前端
1、http status code
2、message
3、error_code:详细,开发者自己定义
4、request_url:当前请求的url
二、错误类型
1、已知型错误
(1)用户传的参数,不符合规则。需要传整型,但是传了字符串。
(2)try… catch
2、未知型错误
(1)程序潜在错误,对开发者来说,是无意识的,或者说开发者根本不知道他出错了
(2)连接数据库,账号、密码输错了
04-06 定义异常返回格式
一、错误返回
app/api/classic.js
if (!query) {const error = new Error('为什么错误')error.error_code = 10001error.status = 400error.request_url = `${ctx.method} ${ctx.path}`throw error}
二、python在两个单词之间用_, error_code
04-07 HttpException异常基类
一、动态,面向对象方式 一个类
二、继承Nodejs内置的Error对象,因为是Error类型的,所以才能throw error
三、
1、core/http-exception.js
class HttpException extends Error {constructor (msg = '服务器错误', errorCode = 1000, code = 400) {super()this.errorCode = errorCodethis.code = codethis.msg = msg}
}module.exports = {HttpException
}
2、middlewares/exceptions.js
const { HttpException } = require('../core/http-exception')const catchError = async (ctx, next) => {try {await next()} catch (error) {if (error instanceof HttpException) { // 已知错误ctx.body = {msg: error.msg,errorCode: error.errorCode,request: `${ctx.method} ${ctx.path}`,}ctx.status = error.code}}
}module.exports = catchError
3、app/api/classic.js
const Router = require('koa-router')
const router = new Router()
const { HttpException } = require('../../../core/http-exception')router.post('/v1/:id/classic/latest', (ctx, next) => {if (true) {// 动态 面向对象方式 一个类const error = new HttpException('为什么错误ne', 10001, 400)throw error}
})module.exports = router
04-08 特定异常类与global全局变量
一、nodejs有个全局变量, global
第05章 LinValidator校验器与Sequelize Orm生成MySQL数据表
05-01 Lin-Validator使用指南
一、api中如果抛出了一个异常,后续的代码是不会执行的
二、LinValidator的npm包名:lin-mizar
git地址:.ts
三、将校验器都放在同一处,app/validators/validator.js
四、validator.js的使用率很高,校验。
文档:.js
05-03 配置文件与在终端显示异常
一、如果在class中,要在constructor中使用this,得要super()
1、没有extends的话,constructor中不用super
二、开发环境需要看到终端异常信息,生产环境不需要看到
config/config.js
module.exports = {enviroment: 'env'
}
core/ int.js
const requireDirectory = require('require-directory')
const Router = require('koa-router')class InitManager {static initCore(app) {InitManager.app = appInitManager.loadConfig()}static loadConfig(path = '') {const configPath = path || process.cwd() + 'config/config.js'const config = require('configPath')global.config = config}
}module.exports = InitManager
三、原型链作业
1、编写一个函数findMember,是最终输出结果为[‘nameA’, ‘nameB’, ‘nameC’, ‘validateC’, ‘ValidateB’, ‘validateA’]
class A {constructor () {this.nameA = 'a'}validateA () {console.log('A')}
}
class B extends A {constructor () {super()this.nameB = 'b'}validateB () {console.log('B')}
}class C extends B {constructor () {super()this.nameC = 'c'}validateC () {console.log('C')}
}// 编写一个函数findMemberconst c = new C()const members = findMembers(c, 'name', 'validate')
console.log(members) // ['nameA', 'nameB', 'nameC', 'validateC', 'ValidateB', 'validateA']
05-04 关系型数据库与非关系型数据库
一、用户系统
1、通用型
2、针对小程序
二、
注册、登录
三、业务的处理通常是放到Model中处理的
1、基本所有的流程走的都是这个流程
四、常用数据库分为两大类
1、关系型数据库 SQL
(1)MySQL
(2)SQLServer
(3)Oracle
(4)PostgresSQL
2、非关系型数据库
(1)Redis
主要是用来做缓存的
(2)MongoDB
存储的是一个一个的类似javascript对象数据,也成为文档型数据库
五、持久存储数据,持久化
六、CRUD, 增删改查
05-05 navicat管理MySQL
一、xampp找到自己对应的版本,就可以不用去官网下载mysql
二、MariaDB是MySQL的分支
三、Navicat for MySQL,数据库可视化管理工具
四、navicat修改密码
1、修改密码
选择任意一个数据库,点击用户
双击后修改
2、是否设置成功
mysql user表,password应该是有值的,如果没有值,则没有设置成功
3、
root@localhost
root@`127.0.0.1`
五、koa中需要设置的用户名/密码,数据库名
数据库名:(自由填写)
默认字符集:utf8mb4
默认排序规则:utf8mb4_general_ci
六、sequelize会将模型自动转换成表
05-06 Sequelize初始化配置与注意事项
一、mysql, sqlserver, postgresdb,oracle这些类型数据库都能使用sequlize
const sequelize = new Sequelize(dbName, user, password, {dialect: 'mysql', // 数据库类型host,port,logging: true,timezone: '+08:00',
}) // 控制或设置数据库的参数,dbName, user, password, js对象()
二、如果数据库类型是mysql,必须安装mysql的驱动
npm i mysql2
三、如果不设置timezone,会跟北京时间相差8小时。
05-07 Use模型与用户唯一标识设计探讨
一、导入的模块重命名
1、es6
import { sequelize as db } from './core/db'
2、commonjs
const { sequelize : db } = require('./core/db')
二、一个用户对一个小程序来说,是不变,且唯一的, openid
一个用户,对小程序,公众号,都唯一的是unionID
三、主键,不能重复,不能为空
四、不要用字符串做主键,用数字类型,查询效率高很多
五、如果是自己用60001, 600002,自己设计的编号作为主键,不能处理并发情况,很有可能计算重复。
六、接口保护,权限,访问接口, Token令牌
app/models/user.js
const { sequelize : db } = require('../../core/db')
const { Sequelize, Model } = require('sequelize')class User extends Model {}User.init({ // mysql一种类型// 主键 关系型数据库// 注册 user id 设计 id编号系统 600001 60002// 自动增长id编号id: {type: Sequelize.INTERGER,primaryKey: true,autoIncrement: true,},nickname: Sequelize.STRING,email: Sequelize.STRING,password: Sequelize.STRING,openid: {type: Sequelize.STRING(64),unique: true,},
}, {
sequelize: db,
tableName: 'user'})
module.exports = {User
}
core/db.js
const Sequelize = require('sequelize')
const { dbName, host, port, user, password } = require('../config/config').databaseconst sequelize = new Sequelize(dbName, user, password, {dialect: 'mysql', // 数据库类型host,port,logging: true,timezone: '+08:00',define: {timestamps: true,paranoid: true,createdAt: 'created_at',updatedAt: 'updated_at',deletedAt: 'deleted_at',underscored: true,},
}) // 控制或设置数据库的参数,dbName, user, password, js对象()sequelize.sync({force: true,
})module.exports = {sequelize
}
05-09 LinValidateor综合应用
一、抛出异常
开发环境,不是httpException
const isHttpException = error instanceof HttpException
const isDev = global.config.enviroment === 'dev'
if (isDev && !isHttpException) {throw error
}
第06章 【构建用户身份系统】通用用户系统与小程序用户系统
06-01 用户注册与Sequelize新增数据
一、如果是自定义的校验中带异步验证,则Lin-Validator需要返回promise,这样可以使用await来先获取校验只,校验过了才能进行下一步。
06-02 中间件只在应用程序启动时初始化一次
一、中间件是一种静态的方式。
二、类,每次都是new一个类,将类实例化,每个api请求,都会实例化一个Lin-Validator
三、不要轻易在中间件以类的形式来组织中间件。函数一般来说,不会实例化。函数通常不会用来保存类的状态,校验器这种复杂的功能比较适合用类的方式来组织,不太适合用函数的方式来组织。
06-03 盐与密码加密的小知识
一、密码加密处理:bcryptjs
导入包的时候,npm包通常是放在第一位
npm i bcryptjs
二、
const salt = bcrypt.genSaltSync(10) // 10:计算机在生成盐的时候所花的成本
三、不能明文存储,即使密码相同,加密后也不能相同
四、
app/api/v1/user.js
const bcrypt = require('bcryptjs')const salt = bcrypt.genSaltSync(10)const psw = bcrypt.hashSync(password, salt)
06-04 模型的set操作
一、app/models
password: {type: Sequelize.STRING,set(val) { // set:观察者模式,如果是值改变,则会自动调用这个方法。es6 reflect方法很容易实现观察者模式const salt = bcrypt.genSaltSync(10)const psw = bcrypt.hashSync(val, salt)this.setDataValue('password', psw) // 赋值的时候会自动调用set方法, setDataValue是Model方法,所以能用this}},
06-05 success操作成功处理
一、全局异常处理throw new global.errs.Success()
core/http-exception.js
class Success extends HttpException {constructor(msg, errorCode) {super()this.code = 201this.msg = msg || 'ok'this.errorCode = errorCode || 0}
}
lib/helper.js
function success(msg, errorCode) {throw new global.errs.Success(msg, errorCode)
}module.exports = {success
}
app/api/v1/user.js
router.post('/register', async (ctx) => {const { body } = ctx.requestconst { email, password, nickname } = bodyconst user = {email,password,nickname,}User.create(user)success()
})
二、ctx.body
router.post('/register', async (ctx) => {const { body } = ctx.requestconst { email, password, nickname } = bodyconst user = {email,password,nickname,}User.create(user)ctx.body = {code: 201,msg: ’这是success的另一种实现方法‘,errorCode: 0,}
})
06-06 isOption校验
一、身份校验
1、session,考虑状态
2、令牌,无状态
(1)token令牌:一串无意义的随机字符串
(2)jwt令牌:携带数据
二、无状态,有状态
rest:一次请求就能拿到数据(无状态)
websrevice :请求:open, 取数据,close (有状态)
三、asp, jsp:动态网页技术
06-07 模拟枚举
一、javascript对象模拟枚举
lib/enum.js
function isThisType (val) {for (let key in this) {if (this[key] === val) {return true}}return false
}
const LoginType = {USER_MINI_PROGRAM: 100,USER_EMAIL: 101,USER_MOBILE: 102,ADMIN_EMAIL: 200,isThisType,
}module.exports = {LoginType
}
二、java可以在编译阶段找到很多问题,javascript, python很多时候只能在执行阶段找问题。
06-08 验证用户账号密码
一、async在函数名的前面
第07章 【主流的用户身份识别方式与权限控制】JWT令牌与Auth权限控制中间件
07-01 jsonwebtoken
一、jwt令牌,npm包:jsonwebtoken
npm i jsonwebtoken
二、core /kɔːr/:核心
core/util.js
const jwt = require('jsonwebtoken')// scope,用来做权限
const generaterToken = function (uid, scope) {const secretKey = global.config.security.secretKeyconst expiresIn = global.config.security.expiresInconst token = jwt.sign({uid,scope,}, secretKey, {expiresIn,})return token
}module.exports = {generaterToken,
}
app/api/v1/token.js
router.post('/', async (ctx) => {const { body } = ctx.requestconst { type } = bodylet tokenswitch (type) {case LoginType.USER_EMAIL:const { account, secret } = bodytoken = await emailLogin(account, secret)ctx.body = {token: token,}}
})async function emailLogin(account, secret) {const user = await User.verifyEmailPassword(account, secret)return generaterToken(user.id, 2)
}module.exports = router
07-02 httpBasicAuth传递令牌
一、token 过期,不合法
二、中间件自带参数ctx, next
三、token放在 body,还是 header ,取悦于前后端约定
四、HTTP 规定 身份验证机制 HttpBasicAuth
basic auth是最基本的
basic64加密
五、HttpBasicAuth需要npm包:basic-auth
npm i basic-auth
六、ctx.req: node.js原生的request对象
ctx.request: koa基于原生noejs的request封装后的request
七、
const basicAuth = require('basic-auth')class Auth {constructor () {}get m() {return async (ctx, next) => {const token = basicAuth(ctx.req)ctx.body = token}}
}module.exports = {Auth
}
八、koa router上可以使用多个中间件。先执行的中间件写在前面,前面的中间件能阻止进入后一个中间件。上一个中间件末尾写await next(),才会去执行第二个中间件
router.get('/latest', new Auth().m, (ctx, next) => {} // m后面不需要加括号,m是一个属性,不是一个方法
07-04 API权限分级控制
一、权限 token角色,普通用户、管理员,可以用分级scope来区分
07-05 小程序openid登录系统
一、业务逻辑
1、在api接口编写
2、model,分层
二、业务分层,
1、nodejs: Model, Service
2、Thinkphp: Model Service Logic
3、java: Model DTO
二、MVC,业务逻辑应该写在M中,model
三、code appid appsecret
07-06 微信鉴权、openid与unionId
一、util是Node.js提供的帮助工具,不需要npm i
const util = require('util')
二、调用微信服务,可以用axios库
const result = await axios.get('=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code')
07-07 根据openid创建微信用户
一、uid,用系统的,不用openid,因为openid是比较机密的数据,不能一直在前后端传输
二、
第08章 使用Lin-UI与在小程序中使用npm
08-01 Lin-UI组件安装
一、微信开发者工具
1、项目设置,选择使用npm模块,调试组件库不要调得太低,尽量调的新的一点
2、开发者工具里的工程根目录,选择“在终端中打开”
(1)要保证是在项目根目录中,现在是在pages 目录下,所以cd …
(2) npm init,这样就创建了package.json文件
(3) npm i Lin-UI,微信开发者工具中不会显示node_modules文件
(4)工具,构建npm。miniprogram_npm,下面会有lin–ui的所有组件
二、小程序对cnpm的支持度比较低
08-02 在小程序中登录
一、如果调用的是本地的api
配置,不校验合法域名
08-04 数据库设计的好思路(实体表与业务表)
一、model code first,
要先考虑的不是数据表,而是model模型
二、主题,由粗到细
1、user
2、期刊
movie
sentence
music
(1)实体,表/model ,记录本身相关信息,事务,表
(2)大千世界事务的映射
3、一期一期, model/表,第1期、第2期、第3期
Flow
(1)很难找到实体,是抽象的,记录业务,解决业务问题,
08-05 Music、Sentence、Movie模型定义
一、classic 共同字段/属性
image
title
pubdate
content
fav_nums
type 代号
(1)url, music特有的
二、共同字段/属性,定义成基类,nodejs中的squelize不能做到直接用类继承,python或java可以
三、
const musicFields = Object.assign({
url: Sequelize.STRING,
}, classicFields)
Music.init(musicFields, {sequelize,tableName: 'music'
} )
08-06 Flow模型与导入SQL数据
一、where是特定的查询条件
二、order,排序
08-07 在小程序中携带令牌
一、base64.js,是一个npm包,可用于base64加码
npm i js-base64
二、
header: {Authorization: this._encode()
}
_encode() {// account: password// token:const token = wx.getStorageSync('token')const base64 = Base64.encode(token + ':')return base64
}
08-08 Sequeliz模型的序列化
一、序列化,对象转换成json
二、art(类)下的dataValues才会被序列化成json,才会被返回
三、对模型进行属性修改
1、
const flow = await Flow.findOne({order: [['index', 'DESC']]
})
const art = await Art.getData(flow.art_id, flow.type)
art.dataValues.index = flow.index // 直接修改了一个类,没有私有成员的概念
ctx.body = art
2、以上方法不推荐,推荐使用以下方法
用内置方法,更为合理,更为安全
art.setDataValue('index', flow.index)
四、sequelize告诉koa框架要通过dataValues进行序列化。
五、json是由js对象抽象出来的
第09章 点赞业务的实现
第10章 面向对象与MySQL in查询
第11章 MySQL group分组查询与JS并发原理
第12章 KOA、Sequelize多几层JSON序列化
第13章 【无感知刷新、获取令牌、登录等】 前后端对接
13-01 小程序如何实现无感知刷新令牌
一、token令牌可能会过期
app.js
import { Token } from 'models/token.js'
App({onLaunch: function () {const token = new Token()token.verify()}
})
二、自动无感知帮助用户重新刷新令牌
退出后,短时间内再进入,不一定会触发onLaunch方法
二次重发机制
13-02 坑!坑!坑!Model中禁止使用构造函数
一、所有的自定义的models中,都不要写constructor
二、参数通过方法参数的方法传过来,而不是通过constrctor传过来
13-03 短评修复
一、实时加载,从onload, 改成onshow,用户切换一下就改变数据
13-04 KOA静态资源
一、静态资源
1、api,读流,返回。
2、不需要api,需要插件(npm包:koa-static)
app.js
const path = require('path')
const static = require('koa-static')
app.use(static(path.join(__dirname, './static')))
二、models处理图片
models/art.js
if (art && art.image) {let imgUrl = art.dataValues.imageart.dataValues.image = global.config.host + imgUrl
}
13-05 image完整路径方案探讨
一、图片拼接,在最源头的地方处理,是最好的
二、以下方法不可行,因为get没办法修改dataValues
models/classic.js
const classicFields = {image: {type: Sequelize.STRING,get() {return global.config.host + this.getDataValue('image')} }
}
三、
const id = art.get('image') // 获取完整路径
const t = art.image // 获取完整路径
const s = art.getDataValue('image') // 获取到原始值
四、一个模型的dataValues是不受get影响的,存储的都是原始字符串。
五、方案
Model.prototype.toJSON 修改
六、hook钩子,最大的好处的是能解耦代码
九、判断是否以http开头
data[key].startsWith('http')
13-06 静态资源存储方案探讨
一、静态资源的加载,图片,最消耗流量
二、静态资源存储
1、网站目录中,app/static/images
2、静态资源服务器,微服务,带宽足够
3、云服务,阿里云OSS(ECS:,RDS:关系型数据库,OSS:云服务)
OSS可以进行CDN缓存
4、免费的静态资源服务器,github, gitpage。
个人服务器可以用。
三、html, css, js可以打包的,都是静态资源
四、vue/react打包出来的资源也是静态资源
nuxt的ssr不属于静态资源,服务端模板渲染
五、适用场景
1、react, vue
(1) CMS,内部管理系统,不需要SEO,可以用react, vue
(2) webApp, h5
2、next的ssr
(1) SEO,B2C技术
13-07 access_token 和refresh_token双令牌保证无感知登录
一、无感知登录
import { Token } from '../models/token.js'const tips = {1: '抱歉,出现了一个错误',1005: 'appKey无效,请前往www.7yue.pro申请'
}class HTTP {request({url,data = {},method = 'GET'}) {return new Promise((resolve, reject) => {this._request(url, resolve, reject, data, method)})}_request(url, resolve, reject, data = {}, method = 'GET', noRefetch = false) {wx.request({url: config.api_base_url + url,method,data,header: {'content-type': 'application/json',Authorization: this._encode()},success: (res) => {const code = res.statusCode.toString()if (code.startsWith('2')) {resolve(res.data)} else {if (code == '403') {if (!noRefetch) {this._refetch(url,resolve,reject,data,method)}} else {reject()const error_code = res.data.error_codethis._show_error(error_code)}}},fail: (err) => {reject()this._show_error(1)}})}_show_error(error_code) {if (!error_code) {error_code = 1}const tip = tips[error_code]wx.showToast({title: tip ? tip : tips[1],icon: 'none',duration: 2000})}_refetch(...param) {var token = new Token()token.getTokenFromServer((token) => {this._request(...param, true)})}_encode() {const token = wx.getStorageSync('token')const base64 = new Base64()const result = base64.encode(token + ':')return 'Basic' + result}
}
二、
三、app
1、缓存中存储app账号,密码
2、双令牌,access_token, refresh_token
(1)access_token,类似于jwt令牌
(2)access_token过期了,通过refresh_token重新获取access_token, 每次发access_token的时候,都重新发送一个refresh_token令牌
四、oAuth2.0,第3方登录的时候用到
第14章 项目部署指南
14-01 部署指南与小程序云开发探讨
一、 部署不属于技巧性的东西
二、本地电脑没有外网ip,可以本地访问,可以局域网访问,但是不能外部访问。
三、云服务,可以理解成linux电脑
四、购买域名之后一定要备案
五、部署
1、服务器、域名
(1)服务器
(2)域名
域名备案
域名解析:域名和ip绑定起来
2、环境安装
mysql:xampp
node
3、nginx 转发
六、通常来说,一个服务,或者一个项目,都有一个端口
七、80端口不需要加在域名后面
八、小程序:https
1、阿里云,有个https证书可以用一年
(1)免费:lets encrypt,每3个月需要续期一次
九、CMS,通常都是web端
十、结合,云开发,api开发
serverless
14-02 守护进程与MP2
一、启动的不能是常规进程,得是守护进程,是后台进程。
1、node app.js启动的是常规进程,会阻塞,关闭terminal终端,进程就会停止。
二、守护进程pm2, 日志监控,重启,安装的时候需要-g安装
启动:pm2 start app.js
终止:pm2 stop app
三、pm2可代替nodemon
第15章 关于Lin CMS和现代大型Web架构思想
本文标签: 笔记
版权声明:本文标题:【笔记 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.betaflare.com/web/1687678144a128107.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论