你好,我是 Jacob,在 2020 这个特殊的年份毕业于上海大学,目前在字节做前端。欢迎来到我的个人博客
jacob-lcs / blog Goto Github PK
View Code? Open in Web Editor NEW一个前端的博客,如果对你有帮助,还请点个 star ✨
Home Page: https://lcs.show
License: MIT License
一个前端的博客,如果对你有帮助,还请点个 star ✨
Home Page: https://lcs.show
License: MIT License
你好,我是 Jacob,在 2020 这个特殊的年份毕业于上海大学,目前在字节做前端。欢迎来到我的个人博客
之前做过一个算法题,算法要求就是写一个开心消消乐的逻辑算法,当时也是考虑了一段时间才做出来。后来想了想,既然核心算法都有了,能不能实现一个开心消消乐的小游戏呢,于是花了两天时间做了一个小游戏出来。
预览地址
先在这里放一个最终实现的效果,还是一个比较初级的版本,大家有什么想法欢迎评论哦
游戏规则:
页面的布局比较简单,格子的数据是一个二维数组的形式,说到这里大家应该已经明白界面是怎么做的了。
<div
v-for="(item, index) in squareData"
:key="index"
class="row">
<div
v-for="(_item, _index) in item"
:key="_index"
class="square"
:class="_item"
@mousedown="dragStart(index, _index)"
@mouseup="dragEnd">
{{_item}}
</div>
</div>
大家应该注意到了 :class="_item"
的写法,动态命名class,使得其每个种类的方块的颜色都不同,最后可以按照同色消除的玩法就行操作。
.square.A{
background-color: #8D98CA;
}
.square.S{
background-color: #A9A2F6;
}
/*其余操作相同*/
同时在玩家点击方块的时候方块会左右摆动以表示选中了此方块,还可以提升游戏的灵动性。关于HTML动画的实现方式有很多,在这里我们使用CSS animation进行操作,代码如下:
@keyframes jitter {
from, 50%, to {
transform: rotate(0deg);
}
10%, 30% {
transform: rotate(10deg);
}
20% {
transform: rotate(20deg);
}
60%, 80% {
transform: rotate(-10deg);
}
70% {
transform: rotate(-20deg);
}
}
/* 只要是用户点击不动,动画就不会停止 */
.square:active{
animation-name: jitter;
animation-duration: 0.5s;
animation-iteration-count: infinite;
}
消除算法
上面提到我之前是做过一道题是判断一个二维数组中有没有可消的元素,有的话是多少个。
在这里我们可以这样想,最开始遍历一整个二维数组,每次定义一个 X0 , X1 , Y0, Y1, 然后每次计算其上下左右连续相同方块的位置,在这个过程中要注意边界问题,然后我们记录下这四个变量,只要 |X0-X1|+1>=3 或者 |Y0-Y1|+1>=3,我们就可以将这个方块的坐标加入到 del
数组中。
遍历完一整个二维数组之后,我们就可以将 del
数组中对应坐标位置的方块内容变为 '0'
, 由于我们没有对 0 定义样式,所以在没有执行下落算法之前变为 0 的方块为白色。
下落算法
在我们将相应的方块白色之后,其上面的方块应该下落,在这里我的**是这个样子的。
按照列遍历二维数组,定义一个指针 t,指向上次不为 0 的方块位置,一旦遇到方块不为 0 的格子就将其与t所指的方块就行交换,一次类推,示意图如下:
这样的话我们就可以把为空的上移到最顶层,并且不打乱顺序,然后我们在随机填充顶部的空方块就可以了。做完填充之后我们要再做一次消除算法,直到del
数组的长度为空为止,这个道理大家应该都能想得到。
代码如下
clear(): void {
const m: number = 10;
const n: number = 10;
while (true) {
const del: any[] = [];
for (let i: number = 0; i < m; i++) {
for (let j: number = 0; j < n; j++) {
if (this.squareData[i][j] === '0') {
continue;
}
let x0: number = i;
let x1: number = i;
let y0: number = j;
let y1: number = j;
while (x0 >= 0 && x0 > i - 3 && this.squareData[x0][j] === this.squareData[i][j]) {
--x0;
}
while (x1 < m && x1 < i + 3 && this.squareData[x1][j] === this.squareData[i][j]) {
++x1;
}
while (y0 >= 0 && y0 > j - 3 && this.squareData[i][y0] === this.squareData[i][j]) {
--y0;
}
while (y1 < n && y1 < j + 3 && this.squareData[i][y1] === this.squareData[i][j]) {
++y1;
}
if (x1 - x0 > 3 || y1 - y0 > 3) {
del.push([i, j]);
}
}
}
if (del.length === 0) {
break;
}
this.score += del.length;
for (const square of del) {
this.$set(this.squareData[square[0]], square[1], '0');
}
for (let j: number = 0; j < n; ++j) {
let t: number = m - 1;
for (let i: number = m - 1; i >= 0; --i) {
if (this.squareData[i][j] !== '0') {
[this.squareData[t][j], this.squareData[i][j]] = [this.squareData[i][j], this.squareData[t][j]];
t -= 1;
}
}
}
}
},
分数为 0 的时候游戏结束,此时在执行一遍初始化函数,重新生成一个开心消消乐格子,将分数初始化为10.
if (this.score <= 0) {
if (confirm('分数用光了哦~~')) {
this.init();
} else {
this.init();
}
}
目前项目是在github上托管,欢迎PR!点此跳转
本文的示例项目源码可以点击 这里 获取
webpack5 也已经发布一段时间了,其模块联邦、bundle 缓存等新特性值得在项目中进行使用。经过笔者在公司实际项目中的升级结果来看,其提升效果显著,热更新时间由原来的 8s 减少到了 2s,会极大的提升开发幸福感。除此之外,webpack5 也带来了更好的 tree shaking 算法,项目的打包体积也会进一步减少,提升用户体验。
目前来看,create-react-app 脚手架还没有适配 webpack5,如果你想熟悉下如何从零开始配置 webpack5 项目的话,不妨跟着文档操作一下。
首先创建一个文件夹,进行 npm 初始化
mkdir react-webpack5-template
cd react-webpack5-template
# npm 初始化配置
npm init -y
# 创建 webpack 配置文件
touch webpack.common.js
# 创建 babel 配置文件
mkdir src && cd src
# 创建入口文件
touch index.js
cd .. && mkdir build
touch index.html
在上述步骤执行完毕之后,你的目录结构应该如下所示:
├── src
│ └── index.js
├── build
│ └── index.html
├── webpack.common.js
├── .babelrc
├── package.json
随后安装必要的依赖
npm i webpack webpack-cli webpack-dev-server html-webpack-plugin babel-loader path -D
npm i react react-dom
文件结构生成完毕后,我们开始编写代码。首先,在index.js
中写入以下代码:
import React from 'react';
import ReactDOM from 'react-dom';
ReactDOM.render(
<React.StrictMode>
<div>你好,React-webpack5-template</div>
</React.StrictMode>,
document.getElementById('root')
);
在 webpack.common.js
中写入以下内容:
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = (env) => {
return {
mode: "development",
entry: {
index: './src/index.js'
},
output: {
// 打包文件根目录
path: path.resolve(__dirname, "dist/"),
},
plugins: [
// 生成 index.html
new HtmlWebpackPlugin({
filename: "index.html",
template: "./build/index.html",
}),
],
module: {
rules: [
{
test: /\.(jsx|js)?$/,
use: ["babel-loader"],
include: path.resolve(__dirname, 'src'),
},
]
},
devServer: {
port: 8080,
host: '0.0.0.0',
},
}
}
在 index.html
中写入以下代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
在 .babalrc
中写入以下代码:
{
"presets": ["@babel/preset-react"]
}
然后在 package.json 中添加如下 script:
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
+ "dev": "webpack serve --config webpack.common.js"
},
随后我们运行 npm run dev
就可以直接运行了,由于我们上面设置的 devServer 端口号为 8080,所以在浏览器中打开 localhost:8080
即可看到如下效果:
到这里位置,我们的初步搭建已经完成了,但是我们在现有的项目中看到的 webpack 配置文件不止这些,有 less、css 文件的解析,image 等资源文件的处理,还有一些优化项的配置等,接下来会一一介绍。
上面我们已经做到可以将一个简单的 React 项目运行起来了,接下来我们要做的是加一些功能。
在前端项目开发过程中,比较经常使用的是 css、less、scss、sass、stylus,下面我们就先仅对 less 进行配置,其余的样式文件可参考 GitHub 源码。首先安装 loader:
npm i style-loader less-loader less css-loader postcss-loader postcss-normalize autoprefixer postcss-preset-env -D
首先,在 webpack.common.js 顶部加入以下正则表达式,用来判断样式文件:
// less/less module 正则表达式
const lessRegex = /\.less$/;
const lessModuleRegex = /\.module\.less$/;
然后在 webpack.common.js 中加入以下配置:
module: {
rules: [
{
test: lessRegex,
use: ["style-loader", "css-loader", "postcss-loader", "less-loader"],
sideEffects: true,
},
]
}
新增 postcss.config.js 文件并配置:
const postcssNormalize = require('postcss-normalize');
module.exports = {
plugins: [
[
"postcss-preset-env",
{
autoprefixer: {
flexbox: "no-2009",
},
stage: 3,
}
],
postcssNormalize(),
require('autoprefixer') ({
overrideBrowserslist: ['last 2 version', '>1%', 'ios 7']
})
],
};
然后我们在 src 目录下新建 index.less 文件,测试配置是否成功:
// index.less
.title {
text-align: center;
color: coral;
}
重新运行项目后发现样式生效,配置成功。
但是仅配置 less 是不够的,我们日常在开发过程中经常用到 less module,在这里我们进行如下配置,首先安装 react-dev-utils
:
npm i react-dev-utils resolve-url-loader -D
在 webpack.common.js 中进行如下配置:
const getCSSModuleLocalIdent = require("react-dev-utils/getCSSModuleLocalIdent");
module: {
rules: [
{
test: lessRegex,
+ exclude: lessModuleRegex,
use: ["style-loader", "css-loader", "postcss-loader", "less-loader"],
sideEffects: true,
},
+ {
+ test: lessModuleRegex,
+ use: [
+ "style-loader",
+ {
+ loader: "css-loader",
+ options: {
+ modules: {
+ getLocalIdent: getCSSModuleLocalIdent,
+ }
+ }
+ },
+ "postcss-loader",
+ "less-loader"
+ ],
+ }
]
}
接下来我们新建 index.module.less 来进行测试:
.font {
color: red;
}
重新运行项目后样式生效,并且 className 也发生了相应变化:
CSS、SCSS 与 SASS 的配置都大同小异,大家可以移步到我的 GitHub。
资源模块(asset module)是一种模块类型,它允许使用资源文件(字体,图标等)而无需配置额外 loader。
在 webpack 5 之前,通常使用:
raw-loader
将文件导入为字符串url-loader
将文件作为 data URI 内联到 bundle 中file-loader
将文件发送到输出目录资源模块类型(asset module type),通过添加 4 种新的模块类型,来替换所有这些 loader:
asset/resource
发送一个单独的文件并导出 URL。之前通过使用file-loader
实现。asset/inline
导出一个资源的 data URI。之前通过使用url-loader
实现。asset/source
导出资源的源代码。之前通过使用raw-loader
实现。asset
在导出一个 data URI 和发送一个单独的文件之间自动选择。之前通过使用url-loader
,并且配置资源体积限制实现。—— 引自 webpack5 中文文档
webpack5 内置 assets 类型,我们不需要额外安装插件就可以进行图片等资源文件的解析,配置如下:
{
test: /\.(jpe?g|png|gif|svg|woff|woff2|eot|ttf|otf)$/i,
type: "asset/resource",
},
如此我们便可以处理引入的图片资源文件,可以根据自身需要进行拓展。
前面提到,webpack5 引入了缓存来提高二次构建速度,我们只需要在 webpack 配置文件中加入如下代码即可开心缓存
cache: {
type: 'filesystem',
// 可选配置
buildDependencies: {
config: [__filename], // 当构建依赖的config文件(通过 require 依赖)内容发生变化时,缓存失效
},
name: 'development-cache',
},
重新运行项目后会发现 node_modules 目录下会新增一个 .cache 文件夹:
笔者在实际项目中测试,热更新时间由原来的 8s 缩短到 2s 可以说是提升巨大。
为了提升构建速度,我们可以引入 thread-loader
提升构建速度,首先我们需要安装:
npm i thread-loader -D
然后在 webpack.common.js
中进行配置:
{
test: /\.(jsx|js)?$/,
- use: ["babel-loader"],
+ use: ["thread-loader", "babel-loader"],
include: path.resolve(__dirname, 'src'),
},
到目前为止,配置工作算是已经完成了,本篇文章只是指导大家进行一些初始化配置,项目中肯定还有很多可以优化的地方,比如说分别配置 webpack.dev.js 以及 webpack.prod.js 以通过测试环境与正式环境的不同需求,在这里就不细说,环境区分的相关配置我会上传到 GitHub 中,如果你觉得项目对你有点用处的话,还请点个 star。
最近工作有一个需求是将一个界面改为响应式布局,由于UI还没有给设计,于是自己先查了一下资料做了一个demo。其实实现响应式布局的方式有很多,利用media实现就是其中一种,但是他也有一些缺点,比如说要对特别的屏幕单独定制样式代码。在我的代码里面我把屏幕分为了三种,代表为iPhone、iPad、PC三种,分别对应着三种不同的样式。
目前可以实现:
- 根据界面大小自动调整布局
- 界面宽度小到一定程度时会隐藏header,将其放到侧拉栏中
效果图如下(代码会在下面全部放上来):
media简单来说就是一种查询工具,加入说你想知道打开你网页的屏幕宽度是768px的时候才使用这个样式,这个时候你就可以这样写:
@media screen and (max-width:768px){
body{
background-color: black
}
}
这个代码的效果就是当前界面的宽度小于768px的时候,将网页背景变成黑色。screen
是用于电脑屏幕、平板电脑、智能手机等。对于@media的更多媒体类型如下:
值 | 描述 |
---|---|
all | 用于所有设备 |
用于打印机或打印预览 | |
screen | 用于电脑屏幕、平板电脑、智能手机等 |
speech | 用于屏幕阅读器等发声设备 |
在做响应式布局的时候我主要用到max-width
和min-width
两种属性,min-width
的作用于max-width
的作用相反。
<link rel="stylesheet" href="./index.css">
<link rel="stylesheet" href="./index_ipad.css" media="screen and (max-width:1200px)">
<link rel="stylesheet" href="./index_mobile.css" media="screen and (max-width:768px)">
由我的代码可以得知我将页面分为三种大小,分别为(1200, +∞),(768, 1200),(0, 768),这个分类我是参照bootstrap来分的。
首先引入index.css,这也是你的电脑打开时的默认样式,当你的电脑宽度逐渐减小时,就会开始应用index_ipad.css这个样式文件,在这个文件中并不是将index.css的样式代码全部重写了一遍,而是把需要更改样式的代码做了编写。
举个例子,比如说我index.css中有四个方块,默认布局是float布局,全部排在一行,但是当页面宽度变为ipad大小是页面方块就会变成两行,原理是改一下方块的宽度。具体实现代码如下:
/* index.css */
.board {
display: flex;
align-items: center;
justify-content: center;
height: 200px;
float: left;
width: 25%;
color: white
}
.first {
background-color: #F44336
}
.second {
background-color: #E91E63
}
.third {
background-color: #9C27B0
}
.fourth {
background-color: #009688
}
/* index_ipad.css */
.first,
.second,
.third,
.fourth {
width: 50%;
}
侧拉栏的原理其实并不难,就是先写一个div,保持与header元素相同,然后再设置其left属性,使其隐藏,通过js操作其left,将其显示出来。
<div class="nav">
<ul>
<li>
<a>第一个</a>
</li>
<li>
<a>第二个</a>
</li>
<li>
<a>第三个</a>
</li>
</ul>
</div>
.nav {
position: absolute;
z-index: 11;
left: -10rem;
top: 0;
width: 10rem;
height: 100%;
background: #607D8B;
}
window.onload = function() {
let btn = document.getElementsByClassName('menu')[0]
let nav = document.getElementsByClassName('nav')[0]
// 改变侧拉栏状态
btn.addEventListener('click', function() {
nav.style.left = nav.style.left == '-10rem' || nav.style.left.length == 0 ? 0 : '-10rem';
}, false);
}
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>响应式布局</title>
<link rel="stylesheet" href="./index.css">
<link rel="stylesheet" href="./index_ipad.css" media="screen and (max-width:1200px)">
<link rel="stylesheet" href="./index_mobile.css" media="screen and (max-width:768px)">
<script src="./index.js"></script>
</head>
<body>
<div class="nav">
<ul>
<li>
<a>第一个</a>
</li>
<li>
<a>第二个</a>
</li>
<li>
<a>第三个</a>
</li>
</ul>
</div>
<nav>
<img src="./img/菜单.png" alt="菜单" class="menu">
<a href="#">第一个</a>
<a href="#">第二个</a>
<a href="#">第三个</a>
</nav>
<div>
<div class="board first">
第一个
</div>
<div class="board second">
第二个
</div>
<div class="board third">
第三个
</div>
<div class="board fourth">
第四个
</div>
</div>
</body>
</html>
/* index.css */
.board {
display: flex;
align-items: center;
justify-content: center;
height: 200px;
float: left;
width: 25%;
color: white
}
.first {
background-color: #F44336
}
.second {
background-color: #E91E63
}
.third {
background-color: #9C27B0
}
.fourth {
background-color: #009688
}
nav {
background-color: #607D8B;
text-align: right;
height: 5vh;
display: flex;
align-items: center;
justify-content: right
}
a {
text-decoration-line: none;
color: white;
margin-right: 3%
}
.menu {
width: 1.5rem;
margin-left: 0px;
display: none;
cursor: pointer;
}
ul,
li {
list-style: none;
padding: 0;
margin: 0;
}
.nav {
position: absolute;
z-index: 11;
left: -10rem;
top: 0;
width: 10rem;
height: 100%;
background: #607D8B;
}
.nav {
transition: left linear .1s;
}
.nav a {
display: block;
padding: 1em 0;
border-bottom: 1px solid #888;
font-size: 16px;
color: #eee;
text-align: center;
}
.nav li {
cursor: pointer;
}
/* index_mobile.css */
.first,
.second,
.third,
.fourth {
float: none;
width: 100%;
}
.menu {
display: block;
margin-right: 2%;
}
a {
display: none
}
/* index_ipad.css */
.first,
.second,
.third,
.fourth {
width: 50%;
}
.menu {
display: block;
margin-right: 2%;
}
a {
display: none
}
//index.js
window.onload = function() {
let btn = document.getElementsByClassName('menu')[0]
let nav = document.getElementsByClassName('nav')[0]
btn.addEventListener('click', function() {
nav.style.left = nav.style.left == '-10rem' || nav.style.left.length == 0 ? 0 : '-10rem';
}, false);
}
最近在写一个批量上传的 node 脚本,其实大家可以想到批量上传带来的接口性能问题,如果将需要上传的文件通过多个异步请求同时上传的话,服务器方面应该是吃不消的,所以这里提出一个 promise 池的概念。
其原理就是固定每次运行的 promise 函数个数,其中有一个完成之后再继续再向这个池子中新增一个 promise 函数运行,这样就可以保持一直有固定个数的 promise 函数在运行,并且服务端的压力不会太大。
具体的实现就像是接力跑步,比如我们规定最多同时执行 5 个异步函数。
赛道上只能有 5 个运动员在跑步,但是要让所有的运动员都跑一次,最好的方法就是 5 个运动员分别在 5 个赛道上跑,谁先跑到终点就将接力棒传递给下一个运动员,这样就能使每个运动员跑一次并且赛道上最多有 5 个运动员。
当然,这里只是说跑到终点会将接力棒传给下一个运动员,但是如果一个运行员跑步过程中受伤了,不能继续完成跑步的话就要放弃,并将接力棒传递给下一个运动员,也就是 promise 函数中的 reject。
还有一个点就是如何监听所有的 promise 函数已经结束,我们在每个 promise 函数运行结束之后都要运行判断一下当前正在执行的函数个数以及还没有运行的函数个数是否为 0,为 0 的话则表示函数运行结束。这里我们使用 EventEmitter 来发送结束消息,表示所有 promise 函数已经执行完成。
具体的实现代码如下:
class PromisePool extends EventEmitter {
constructor(params, fn, max) {
super();
this.params = params; // 异步函数的参数数组
this.fn = fn; // 异步函数
this.max = max; // 同时运行的最大数量
this.pool = []; // 存放task
this.successNum = 0; // 成功个数
this.failNum = 0; // 失败个数
this.count = params.length; // 当前正在运行的异步函数个数
this.init();
}
// 初始化函数
init() {
while (this.count < this.max) {
const param = this.params.shift();
if(!param){ break; }
this.genTask(param);
}
}
genTask(param) {
this.count++;
const task = this.fn(param);
task.then(() => {
this.successNum++;
}).catch(() => {
this.failNum++;
}).finally(() => {
this.count--;
this.init();
if(this.count === 0 && this.params.length === 0){
this.emit('finish');
}
})
}
}
我们先写一个简单的HTML文件,方便我们接下来进行效果实现
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="stylesheet" href="./index.css">
<title>水平垂直居中</title>
</head>
<body>
<div class="container">
<div class="inner">水平垂直居中</div>
</div>
</body>
</html>
/* CSS文件 */
.container{
width: 400px;
height: 400px;
border: 1px solid black;
}
.inner{
width: 100px;
height: 100px;
border: 1px solid red;
}
最终浏览器呈现的效果如下
水平居中的方式有多种,总体来说可以使用flex
、grid
、text-align
、margin
等方法,我们要实现如下所示的效果
.container{
width: 400px;
height: 400px;
border: 1px solid black;
/* 新增 */
display: flex;
justify-content: center;
}
只需在父DOM元素中新增以上CSS即可实现水平居中
.inner{
width: 100px;
height: 100px;
border: 1px solid red;
/* 新增 */
margin: 0 auto;
}
**注:**这个方法适用于知道子dom元素宽度已知的情况下
要注意:
text-align
CSS属性定义行内内容(例如文字)如何相对它的块父元素对齐,并不控制块元素自己的对齐,只控制他行内内容的对齐。
所以这个属性对于我的HTML文件是无效的,我们要把内部的div改为span
即可生效,修改后的代码如下:
<body>
<div class="container">
<!-- 将div修改为span -->
<span class="inner">水平垂直居中</span>
</div>
</body>
CSS样式表修改为如下所示
.container{
width: 400px;
height: 400px;
border: 1px solid black;
/* 新增 */
text-align: center;
}
.container{
width: 400px;
height: 400px;
border: 1px solid black;
/* 新增 */
display: grid;
grid-template-rows: 1fr; /* 让inner的高度占满 */
grid-template-columns: 1fr; /* 让inner的宽度占满 */
justify-items: center; /* 让inner水平居中 */
}
关于grid
的更多介绍详见张鑫旭的:point_right:博客👈
.container{
width: 400px;
height: 400px;
border: 1px solid black;
/* 新增 */
position: relative;
}
.inner{
width: 100px;
height: 100px;
border: 1px solid red;
/* 新增 */
position: absolute;
left: 50%;
transform: translateX(-50%);
}
这个地方要注意的是,如果想要使用left
、right
、top
、bottom
方法的时候必须保证父dom的position
为relative
属性,子dom
的position
为absolute
属性,这样才可以生效。并且子dom
的基准是离其最近的position
属性为relative
的父dom
元素
当然,这里你如果很明确子dom元素和父dom元素的宽度的话,比如我写的HTML代码,可以直接使用left: 150px
来实现,但是这样的话就没有什么拓展性了。
垂直居中的方法也有很多,比如flex
、top
、grid
等,我们要实现的效果如下
.container{
width: 400px;
height: 400px;
border: 1px solid black;
/* 新增 */
display: flex;
align-items: center;
}
只需在父DOM元素中新增以上CSS即可实现垂直居中、
.container{
width: 400px;
height: 400px;
border: 1px solid black;
/* 新增 */
display: grid;
grid-template-rows: 1fr;
grid-template-columns: 1fr;
align-items: center;
}
.container{
width: 400px;
height: 400px;
border: 1px solid black;
/* 新增 */
position: relative;
}
.inner{
width: 100px;
height: 100px;
border: 1px solid red;
/* 新增 */
position: absolute;
top: 50%;
transform: translateY(-50%);
}
使用line-height
方法是不能使我这个HTML垂直居中的,但是如果你想垂直居中的元素是一个单行文本,那么可以参照这个方法。
<body>
<div class="container">
<!-- 将div修改为span -->
<span class="inner">水平垂直居中</span>
</div>
</body>
.container{
width: 400px;
height: 400px;
border: 1px solid black;
}
.inner{
width: 100px;
line-height: 400px;
border: 1px solid red;
}
请注意一定是单行文本,因为line-height是指一行文本的高度,如果是两行文本的话就不能实现垂直居中的效果了
.container{
width: 400px;
height: 400px;
border: 1px solid black;
}
/* 新增 */
.container::after{
display:inline-block;
vertical-align:middle;
content:'';
height:100%;
}
.inner{
width: 100px;
height: 100px;
border: 1px solid red;
/* 新增 */
display: inline-block;
vertical-align: middle;
}
以上就是我对于垂直居中方法的一些整合,如果您有什么其他的方法,请在评论区进行讨论!
最近笔者在复习 JavaScript 基础知识,刚看完 《JavaScript 高级程序设计(第四版)》,想再找一些优秀代码库巩固一下学到的内容,自然而然得就想到了 Lodash
。
Lodash
是一个一致性、模块化、高性能的 JavaScript 实用工具库。在笔者的公司内使用率颇高,相信在大家的项目中也是如此,于是就想研究一下 lodash
源码,顺便做一些源码分析,独乐乐不如众乐乐,写一些文章出来与大家分享。
于是就到了这篇文章的主题,快速搭建一个文档网站。其实笔者是有一个服务器的,域名是 https://lcs.show
,但是服务器确实带宽有限,再加上还得自己配置 NGINX、GitHub Action 以及 https 证书等内容。
很巧看到了腾讯云 cloudbase 服务,可以快速搭建静态网站,如果你没有域名的话会自动分配一个域名(但是会比较难记),可以的话还是自己注册一个域名,也可以很方便地申请并配置 https 证书,如果搭配 GitHub Action 使用的话可以说是完全不用考虑服务器维护的内容了。
接下来就来讲一下如何搭建以及部署。
本文是以 VuePress 为例进行搭建部署,VitePress、Next、Docsify 等部署大同小异。
npm install -g @cloudbase/cli@latest
tcb new cloudbase-test vuepress
使用该 CLI 是需要进行登录的,如果 CLI 检测到你当前没有登录的话会自动打开浏览器跳转到腾讯云登录页面,登录成功后返回命令行,继续下一步操作:
接下来选择你认为合适的一个服务器地点,在这里我选择上海。
接下来会选择关联环境,如果你当前没有环境的话可直接选择「创建新环境」,CLI 会自动打开浏览器跳转到「创建新环境」页面,创建新环境如下图,在这里我选择使用 VuePress 模板进行创建:
选择完成后点击下一步即可:
创建成功后返回命令行,会显示正在初始化环境,稍等几分钟就可以直接创建项目。创建成功后会生成以下目录结构的项目:
├── README.md
├── cloudbaserc.json
├── guides
│ └── README.md
└── package.json
npm i
npm run deploy
执行 npm run deploy
稍等片刻之后即可部署成功,命令行会返回一个访问域名,笔者的为 https://cloudbase-test-9gccjnk3e393c02a-1256377994.tcloudbaseapp.com/vuepress/ ,点击即可访问示例网站,如下:
到这里为止,其实部署工作就算是结束了,无需自己配置 NGINX 等复杂繁琐的操作,这就是云服务的魅力,同时该服务按量计费,对于笔者来说费用可以说是非常低了。
但是,仅此还不够,我们要配置 GitHub Action 之后,才能算完全放手部署这件事,做到完全自动化,将日常工作精力专注于文档编写就可以了。
如果初始化了一个项目的话,会看到项目中有一个 cloudbaserc.json
文件,该文件为 cloudbase 配置文件,文件中有一个 envId
配置项,这属于敏感信息,请注意千万不要上传到 GitHub 中,将该配置信息从 cloudbaserc.json
中删除!
既然不能上传的话,我们应该如何配置呢,答案很简单,使用 GitHub secret 即可。需要在 腾讯云控制台 新建秘钥,新建完成后,打开你的 GitHub 仓库进行如下设置:
创建 ENVID、SECRETID、SECRETKEY 三条 secret,其中 ENVID 在 应用列表中可见,配置完成后如下所示:
接下来在项目的 .github/workflows
目录下创建 deploy.yml
文件,内容如下:
name: 自动化部署
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
deploy:
runs-on: ubuntu-latest
name: deploy
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Deploy to Tencent CloudBase
uses: TencentCloudBase/cloudbase-action@v2
with:
secretId: ${{ secrets.SECRETID }}
secretKey: ${{ secrets.SECRETKEY }}
envId: ${{ secrets.ENVID }}
然后将你的项目代码 push 到 GitHub 就可以完成自动化部署了,之后就可以专心进行文档编写,无需关心服务器维护这样的事情了。
快乐搬砖~
像腾讯云 cloudbase 这样的云服务可以说真的方便了很多,可以直接部署自己的静态博客或者文档等站点,一键部署,无需运维,岂不美哉。
前面说到笔者最近正在写 lodash 源码解析,地址是 lodash.lcs.show,GitHub 地址为 https://github.com/jacob-lcs/lodash-source-code-analysis当然还处于刚开始的阶段,大家有兴趣的话欢迎关注~
整体学习Vue时看到Vue文档中有事件修饰符的描述,但是看了之后并没有理解是什么意思,于是查阅了资料,现在记录下来与大家分享
先给大家画一个示意图理解一下冒泡和捕获
.stop
修饰符请看如下代码
<template>
<div class="about">
<div @click="test1">
<div @click="test2">
测试
</div>
</div>
</div>
</template>
<script>
export default {
methods:{
test1(){
console.log('test1')
},
test2(){
console.log('test2')
}
}
}
</script>
由以上代码可以看到我们有一个嵌套的div,每一个div都绑定着一个事件,如果我们点击div的话是按什么顺序触发这两个事件的呢。其实是默认按照冒泡的方式触发的,简单来说就是由内而外,如果还是不明白请看上面的解析图。
此Vue文件最终生成的界面是这个样子的
当我们点击的时候默认按照冒泡方式触发函数,控制台打印结果如下
现在就是.stop
发挥作用的时候了,修改代码如下
<template>
<div class="about">
<div @click="test1">
<div @click.stop="test2">
测试
</div>
</div>
</div>
</template>
这样我们在点击之后控制台打印结果如下
由这个结果我们可以看到,这个修饰符的作用就是阻止事件冒泡,不让他向外去执行函数,到此为止
.prevent
修饰符这个时候我们再来说一下.prevent修饰符,其作用就是阻止组件本来应该发生的事件,转而去执行自己定义的事件
<template>
<div class="about">
<a href="https://www.cnblogs.com/Jacob98/" @click="test2">跳转</a>
</div>
</template>
<script>
export default {
methods:{
test2(){
console.log('test2')
}
}
}
</script>
上述代码我们并没有添加.prevent修饰符,接下来的结果我们应该可以想到,点击之后会跳转到我写的网址中(也就是我的博客:sunglasses:),当时当我们对这个代码稍作修改
<template>
<div class="about">
<a href="https://www.cnblogs.com/Jacob98/" @click.prevent="test2">跳转</a>
</div>
</template>
点击之后就不会跳转到相应的网址,而是去执行我写的函数
.capture
修饰符其实这个的理解就很简单,就想我们第一节所说,网页是默认按照冒泡方式去触发函数的,但是当我们使用.capture修饰符时,网页就会按照捕获的方式触发函数,也就是从外向内执行,但是这个时候一定要注意,.capture修饰符一定要写在外层才能生效,原因大家应该能自己想清楚。
<template>
<div class="about">
<div @click.capture="test1">
<div @click.stop="test2">
测试
</div>
</div>
</div>
</template>
控制台打印结果如下
.once
修饰符这个理解起来就更加简单了,加上此修饰符之后相应的函数只能触发一次,无论你点击多少下,函数就只触发一次。这个有一个用途就是防止用户多次点击造成应用数据错误。比如说用户点击支付按钮,由于客户机器比较卡顿,点击一下之后没有立即反应就又点了一下,这个时候有可能就会造成多次扣费,降低用户体验。
.self
修饰符当前元素自身时触发处理函数时才会触发函数,原理:是根据event.target确定是否当前元素本身,来决定是否触发的事件/函数
实例:如果点击内部点击2,冒泡不会执行gett方法,因为event.target指的是内部点击2的dom元素,不是外部点击1的,所以不会触发自己的点击事件
<div v-on:click.self="test1">
test1
<div v-on:click="test2">
test2
</div>
</div>
在做需求过程中我们大概率会遇到在浏览器中下载文件的需求,如果仅仅是这个要求的话很简单,有如下两种解决方式。
第一种是通过 window 对象的 open 方法进行操作,将文件 url 直接在浏览器中打开即可下载。
window.open('url')
第二种是通过 a 标签,设置 href 为 url 值,点击 a 标签即可完成下载。
<a href='url' download='文件名'></a>
但是上面两种文件下载方式都会存在一个问题,就是 pdf 文件会直接在浏览器中打开而不是直接下载,效果如下:
这种需求的解决方式就是将PDF文件的 MIME type 改为 application/octet-stream
并加入 Content-Disposition:attachment
header,原本的 pdf 文件 MIME type 为 application/pdf
,浏览器识别到这个 type 之后会自动在浏览器打开,所以说我们在这里修改 type 即可。
修改的方法有两种,一种是在后端进行修改,上传文件或者返回文件的时候进行操作,但是绝大多数情况下文件都是存储到 cdn 服务器中的,后端也不方便对其进行操作,这个时候就需要前端来修改了。
处理代码如下:
/**
* @deprecated 下载文件
* @param {string} url
* @param {string} filename
*/
handleFileDownload = (url, filename) => {
// 创建 a 标签
let a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
}
/**
* @deprecated 处理 pdf url,使其不在浏览器打开
* @param {string} url
*/
handlePdfLink = (url, filename) => {
fetch(url, {
method: 'get',
responseType: 'arraybuffer',
})
.then(function (res) {
if (res.status !== 200) {
return res.json()
}
return res.arrayBuffer()
})
.then((blobRes) => {
// 生成 Blob 对象,设置 type 等信息
const e = new Blob([blobRes], {
type: 'application/octet-stream',
'Content-Disposition':'attachment'
})
// 将 Blob 对象转为 url
const link = window.URL.createObjectURL(e)
handleFileDownload(link, filename)
}).catch(err => {
console.error(err)
})
}
大致编写的HTML界面渲染后是这个样子的,我们现在想要实现的需求是点击Button
所在的div不会触发事件,而在点击Button
所在的div之外的区域时会触发事件,下面就来介绍三种方法实现。
<!-- HTML代码 -->
<html>
<head>
<link rel="stylesheet" href="./index.css">
</head>
<body>
<div class="container">
<div class="inner">
Button
</div>
</div>
<script src="./index.js"></script>
</body>
</html>
/* CSS代码 */
.container{
width: 400px;
height: 400px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid black;
}
.inner{
height: 100px;
width: 100px;
background-color: burlywood;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
}
由上面的HTML代码可以看到我们有一个嵌套的div,如果我们点击div的话是按什么顺序触发这两个事件的呢。其实是默认按照冒泡的方式触发的,简单来说就是由内而外,如果还是不明白请看上面的解析图。这就导致我们点击Button
所在的div也会触发事件,所以我们要阻止冒泡就可以实现我们的需求
/**
* 方法一
* 利用阻止事件冒泡实现
*/
const inner = document.getElementsByClassName('inner')[0];
const container = document.getElementsByClassName('container')[0];
inner.addEventListener('click', event => {
event.stopPropagation(); // chromium内核
window.event.cancelBubble = true; // IE内核
})
container.addEventListener('click', event => {
console.log('success');
})
/**
* 下面介绍了三种方法来判断是否为内部元素
* 1.className是否相等,也可以使用id
* 2.DOM元素是否相等
* 3.点击的DOM是否包含内部DIV
*/
container.addEventListener('click', event => {
if('inner' !== event.target.className) {
console.log(`success`);
}
if(inner !== event.target) {
console.log('success');
}
if(event.target.contains(inner) && event.target !== inner) {
console.log('success');
}
})
.self
修饰符当前元素自身时触发处理函数时才会触发函数
原理:是根据event.target确定是否当前元素本身,来决定是否触发的事件/函数
<template>
<div id="app" @click.self="container">
<img alt="Vue logo" src="./assets/logo.png" >
</div>
</template>
<script>
export default {
name: 'app',
components: {
HelloWorld
},
methods:{
container() {
console.log('success')
}
}
}
</script>
<style>
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
background-color: aqua;
}
</style>
这样的话也可以实现点击内部div之外的部分触发特定函数
大家有什么其他的方法可以实现欢迎评论内提出:smile:
Web 端:https://github.com/jacob-lcs/awesome-curriculum-web
Android 端:https://github.com/jacob-lcs/awesome-curriculum-android
Nodejs 端:https://github.com/jacob-lcs/awesome-curriculum-backend
预览网址:http://schedule.lcs.show/
自己在大学的时候做了一个课程管理与推荐系统,系统的功能包括课程表、课程推荐、课程群聊三大功能。其实自己也是在大学时候感受到没有一款相似的工具出现,并且平时想找大学课程班上的同学比较困难,因为大学是走班制,没有固定的班级。所以就做了这么一款软件。总体来说分为 Android 端、Web 端、Nodejs 端。
本章主要来介绍一下本系统做的一些功能,并且通过 gif 图片的形式对其作出演示。
课程表就是可以通过自动导入或者手动新建的方式进行添加,但是由于浏览器跨域限制,Web 端并没有做自动导入功能,而是将其放到了 Android 端,GitHub 地址如上所示。自动导入故名思义,输入你的学号和密码,就可以通过学校教务处拉取你的课程信息,当然这个需要适配,目前只适配了上海大学(对,我就是上海大学的😝)。
手动新增课程的方式也比较人性化,通过在课程表中下拉就可以弹出课程详情的 form 表单,输入相关的信息即可。下面通过 gif 图片来演示一下。
课程群聊就是在你新建完课程之后,系统会自动根据你新建的课程将你拉入到对应的课程群聊当中,省去了寻找群聊的过程,打开聊天界面即可看到所有课程群聊,要注意的是当学校、课程名称、课程好均相同时才会进入到同一个课程群聊当中。
在群聊中可以发送图片、表情、文字等信息,演示如下:
系统中还有一个课程推荐的功能,就是根据你平时的点击日志进行课程推荐,推荐算法使用的是比较经典的基于物品的协同过滤推荐算法。数据库的所有课程都是从网易云课堂、腾讯课堂、MOOC 等网站爬取的,具体的方法可以看我这篇博客,演示如下:
安装依赖
yarn install
运行项目
yarn start
你应该可以发现项目目录/config/
下的三个文件里面的配置项都为空,因为我使用的云服务器作为 MySQL 数据存储,包括 qq 邮箱密钥,为了保护隐私,还请大家自己填写调试程序。
// PASS_SECERT.js
const PASS_SECRET = {
SECRET_KEY: "" // 加密密码的密钥,自己随便填写就好
};
// dbConfig.js
const dbConfig = {
DATABASE: "", //数据库
USERNAME: "", //用户
PASSWORD: "", //密码
PORT: "", //端口
HOST: "" //服务ip地址
};
// email.js
const emailInfo = {
user: "",
pass: "" // QQ邮箱密钥,注意:不是密码
};
另外,config 目录下还有两个 pem 文件,这个是生成的密钥和公钥,请按照以下方法生成
genrsa -out rsa_private_key.pem 2048
rsa -in rsa_private_key.pem -pubout -out rsa_public_key.pem
npm run install
npm run start
ReactColor
是一个优秀的 React 颜色选择器组件,官方给了多种布局供开发者选择。
笔者常用的主题为 Sketch,这种主题涵盖了颜色面板、推荐色块、RGB颜色输入等功能,比较完善。但是最近在写一个富文本编辑器,编写过程中遇到了一些问题,比如用户在点击推荐色块时,编辑器会失去焦点,无法对字体颜色进行更改。如果是编辑器自有的组件,可以使用以下代码
event.preventDefault();
该代码可以禁止浏览器默认行为,比如点击推荐色块之后只将色值向上传递,而不改变浏览器当前 focus
状态。但是 ReactColor
并没有暴露该事件,故 clone 了源码,在编辑器内集成了该组件,实现功能的同时也能够减少打包体积。
本章节主要介绍 ReactColor
的实现原理,以比较有代表性的 Sketch 主题为例。
由上图可以看到,整个颜色选择器面板由这六个部分组成,分别是亮度与饱和度调节面板、色相 Hue 调节面板、透明度调节面板、当前颜色的 RGBA 与 Hex 值、推荐色块以及颜色实时预览。下面的部分就来介绍其原理实现。
与颜色相关的几个属性分别为亮度、饱和度、色相与透明度,与我们平时用到的 RGB 色彩模型不同,ReactColor
中用的是 HSV 色彩模型,其具体含义如下:
下面是维基百科对 HSV 色彩模型的介绍:
HSV即色相、饱和度、明度(英语:Hue, Saturation, Value),又称HSB,其中B即英语:Brightness。
至于为什么选用 HSV 色彩模型而不是直接使用 RGB,大家在使用 ReactColor 的过程中应该会发现,只要在下方的 色相 Hue 调节面板上选中了颜色,亮度与饱和度调节面板就会呈现什么颜色。举个例子:你选择了黄色,那么最上方调节面板呈现的就是黄色,差别也只是饱和度与明度不同而已。这就是使用 HSV 色彩模型的优势,让用户选择的颜色变成可预知并且方便调节的。
RGB 颜色空间利用三个颜色分量的线性组合来表示颜色,任何颜色都与这三个分量有关,而且这三个分量是高度相关的,所以连续变换颜色时并不直观,想对图像的颜色进行调整需要更改这三个分量才行。自然环境下获取的图像容易受自然光照、遮挡和阴影等情况的影响,即对亮度比较敏感。而 RGB 颜色空间的三个分量都与亮度密切相关,即只要亮度改变,三个分量都会随之相应地改变,而没有一种更直观的方式来表达,而这就是 HSV 色彩模型的优势所在。
上面提到,在日常的前端开发过程中还是普遍使用 RGB 色彩模型进行颜色表示,在用户设置好 HSV 值后我们需要将其转为 RGB 值,公式如下(该公式来自维基百科)
$$rgb=\begin{cases}
(v,t,p), & \text{if
(q,v,p), & \text{if
(p,v,t), & \text{if
(p,q,v), & \text{if
(t,p,v), & \text{if
(v,p,q), & \text{if
\end{cases}$$
这样在用户选择完成后就可以对色彩空间实时转换,通过 onChange
回调返回给用户。
既然使用了 HSV 色彩模型就要考虑一下如何表示这三个变量,下面我们分两部分来讲。
颜色名称 | 红绿蓝含量 | 角度 | 代表物体 |
---|---|---|---|
红色 | R255,G0,B0 | 0° | 血液、草莓 |
橙色 | R255,G128,B0 | 30° | 火、橙子 |
黄色 | R255,G255,B0 | 60° | 香蕉、杧果 |
黄绿 | R128,G255,B0 | 90° | 柠檬 |
绿色 | R0,G255,B0 | 120° | 草、树叶 |
青绿 | R0,G255,B128 | 150° | 军装 |
青色 | R0,G255,B255 | 180° | 水面、天空 |
靛蓝 | R0,G128,B255 | 210° | 水面、天空 |
蓝色 | R0,G0,B255 | 240° | 海、墨水 |
紫色 | R128,G0,B255 | 270° | 葡萄、茄子 |
品红 | R255,G0,B255 | 300° | 火、桃子 |
紫红 | R255,G0,B128 | 330° | 墨水 |
如何横向表示色相呢,只需要一行 CSS 代码:
background: linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%);
这样即可大致表达出 0-360 度的色相值,效果如下:
根据鼠标拖动的位置距离左边界的距离就可以计算出色相值。
/**
* 在颜色值发生变化时实时计算相应的色相值
* @param event
*/
const handleChange = (event: any) => {
if (!ref.current) {
return;
}
const clientRect = ref.current.getBoundingClientRect();
const { width: containerWidth } = clientRect;
const x: number = typeof event.pageX === 'number' ? event.pageX : event.touches[0].pageX;
const left = x - (clientRect.left + window.pageXOffset);
let innerHue;
// 处理边界值
if (left < 0) {
innerHue = 0;
} else if (left > containerWidth) {
innerHue = 359;
} else {
const percent = (left * 100) / containerWidth;
innerHue = (360 * percent) / 100;
}
setHue(innerHue);
props.onChange({ h: innerHue });
};
**饱和度(S)**是指色彩的纯度,越高色彩越纯,低则逐渐变灰,取0-100%的数值。**明度(V)**指颜色的亮度,不同的颜色具有不同的明度。
在 ReactColor 中按照如下方式来表示饱和度与明度。
其实用 CSS 表示也比较简单,使用渐变色来表示就可以实现该效果。
background: linear-gradient(to right, #fff, rgba(255, 255, 255, 0));
background: linear-gradient(to top, #000, rgba(0, 0, 0, 0));
与色相的计算方式一样,也是根据鼠标拖动的位置距离左边界和下边界的距离来计算,计算方法可以参考色相的思路。
大家看完这篇文章应该发现代码部分其实我介绍的不多,更多还是介绍 HSV 色彩模型,以及作者为什么没有使用 RGB 表示。
如果大家去看 react-color 源码就会发现代码其实不难理解,难点还是在 HSV 的应用方法上面,大家如果有需要自己在项目里面定制化颜色选择器的话也可以根据这个思路来,一天之内就可以写出来。
本文的所有代码都在GitHub上托管,想要代码的同学请点击这里:smile_cat:
序:由于自己想要实现一个课程推荐系统,需要在各大视频网站上爬取所有视频课程,从而为后续的推荐工作提供大量数据,在此篇博客中我分别爬取了MOOC、网易云课堂、腾讯课堂、学堂在线共约15万条数据。
运行环境:
mysqlclient
~=1.4.6
requests
~=2.22.0
bs4
~=0.0.1
beautifulsoup4
~=4.8.2
首先进入网站,在这里我们分析他的API设计,先要找到他是从哪一个API获得相应课程的,经过分析之后我们发现是https://www.icourse163.org/web/j/courseBean.getCoursePanelListByFrontCategory.rpc
这个API,其返回内容如下:
然后我们随意点击页面上的一个课程,找到其课程url的规律,打开沟通心理学这门课程,其URL是https://www.icourse163.org/course/HIT-1001515007
,而沟通心理学这门课程返回的信息是:
// 在这里我只保留了我需要的一些数据
{
name: "沟通心理学",
id: 1001515007,
schoolPanel: {id: 9005, name: "哈尔滨工业大学", shortName: "HIT"}
}
我们可以发现课程的URL就是学校的简称-id,这样就可以组成课程URL,现在我们得知课程URL如何得知,那么这些课程数据需要传什么参数呢,如下:
{
categoryId: -1, // 类别id,因为我这里选的全部,所以是-1
type: 30,
orderBy: 0,
pageIndex: 1, // 第几页
pageSize: 20 // 每页多少条数据
}
到这里就新产生了一个问题,categoryId
是怎么来的,我们继续看网页请求的api列表,找到这样一个APIhttps://www.icourse163.org/web/j/mocCourseCategoryBean.getCategByType.rpc
,其返回结果如下:
我们想要得到的课程分类特别细致的话就需要一直向下找json的children,直到children为空,算法的话就采用递归算法就可以。
到现在为止我们已经知道了如何获取类别id,如果由类别id获得课程数据,接下来我们就需要把获取到的数据存储到数据库中,我的数据库包含类别、课程名称、课程图片URL、课程URL、课程来源这四个字段,存储代码如下:
# 存储到数据库
def save_to_mysql(data, category_name):
sql = "insert into webCourses (category, name, site, imgUrl, resource) values ('{0}', '{1}', '{2}', '{3}', '{4}')".format(
category_name, data["name"],
'https://www.icourse163.org/course/' + str(data["schoolPanel"]["shortName"]) + "-" + str(data["id"]),
data["imgUrl"], "慕课")
print(sql)
execute(sql)
要注意这里的execute
函数是我封装的一个函数,具体的作用就是运行sql语句,全部代码请到我的GitHub查看。
其实如果你看过了上面MOOC的获取所有课程的API设计,其他课程网站的API设计也是大致相同的,首先我们要获得类别id,然后再根据类别id去请求数据,与mooc不同的是腾讯课堂请求课程数据是通过beautifulsoup4
解析html
内容实现的。下面就来简单说一下:
获取课程类别的API:https://ke.qq.com/cgi-bin/get_cat_info
根据类别id获得数据的网页url: https://ke.qq.com/course/list?mt=1001&st=2001&tt=3001&page=2
,这里的mt、st、tt分别是三个类别,st是mt的一个子类,tt是st的一个子类,page就是页数了。
在这里我们需要的是每一个课程,其实思路很简单,按F12
打开开发者工具,找到课程对应的dom块,用beautifulsoup4
解析html
内容,得到课程数组就可以了,代码如下:
# 获取课程数据
def get_course_data(mt, st, tt, page, category):
url = "https://ke.qq.com/course/list?mt=" + str(mt) + "&st=" + str(st) + "&tt=" + str(tt) + "&page=" + str(page)
response = requests.request("GET", url).text
bs = BeautifulSoup(response)
course_blocks = bs.find_all(name='li', attrs={"class": "course-card-item--v3 js-course-card-item"})
# print(course_blocks)
if len(course_blocks) != 0:
for i in range(len(course_blocks)):
bs = course_blocks[i]
img = bs.find(name="img", attrs={"class", "item-img"})
a = bs.find(name="a", attrs={"class", "item-img-link"})
save_to_mysql(img.attrs["alt"], a.attrs["href"], img.attrs["src"], category)
return True # 这里是返回该类别的下一页是否还有数据
else:
return False
得到数据之后再将这些数据存入到数据库中就可以了。
其实网易云课堂就和MOOC的API设计非常类似了,毕竟都是网易公司的程序员写的,规范也都差不多,看懂MOOC的api设计的同学直接去我的github看代码就可以了。
学堂在线的API设计就比较简单,直接通过一个API就可以获得所有的数据,https://next.xuetangx.com/api/v1/lms/get_product_list/?page=1
,返回的数据格式如下:
在这里一个API里面课程名称、分类、课程封面URL,课程id可以看的非常请求,下面我们就需要得到课程信息与课程url之间的关系,我们随意点开一个课程,分析他的URL,我们就可以发现,课程URL就是https://next.xuetangx.com/course/
加上课程的course_sign
组成的。
到这里就分析结束,存储到数据库即可。
从上述的分析我们可以看出,各大课程网站的api设计都是类似的,并且他们都没有做api请求限制,所以我在爬取过程中没有遇到过被封IP的情况,也算是省了不少事:joy:。在这里把代码与思路分享给大家,希望能够给到大家一些帮助。所有代码请点击这里:smile_cat:
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.