刘宁Leo

刘宁Leo 查看完整档案

北京编辑西安电子科技大学  |  软件工程 编辑顺丰  |  研发工程师 编辑 liuningfr.github.io 编辑
编辑

法国留学的前端菜狗子,想跟大家一起成长。

个人动态

刘宁Leo 收藏了文章 · 2020-02-17

要去大厂应该把这些面试题搞(bei)懂(hui)

阳春召我以烟景,大块假我以文章,掘金予我以面经 - 李白 春夜宴从弟桃李园序

这是我自己在仓库中的总结:https://github.com/shfshanyue...

高赞文章收藏

如果这些还不够,我在 github 上新建了一个仓库 日问,每天一道面试题,有关前端,后端,devops以及软技能,促进职业成长,敲开大厂之门,欢迎交流

勤学如春起之苗,不见其长,日有所增

并且记录我的面试经验

我是山月,可以加我微信 shanyue94 与我交流,备注交流。另外可以关注我的公众号【全栈成长之路】

如果你对全栈面试,前端工程化,graphql,devops,个人服务器运维以及微服务感兴趣的话,可以关注我

查看原文

刘宁Leo 收藏了文章 · 2019-01-25

git代码统计

命令行

查看git上的个人代码量:

git log --author="username" --pretty=tformat: --numstat | awk '{ add += $1; subs += $2; loc += $1 - $2 } END { printf "added lines: %s, removed lines: %s, total lines: %s\n", add, subs, loc }' -

结果示例:(记得修改 username)

added lines: 120745, removed lines: 71738, total lines: 49007

统计每个人增删行数

git log --format='%aN' | sort -u | while read name; do echo -en "$name\t"; git log --author="$name" --pretty=tformat: --numstat | awk '{ add += $1; subs += $2; loc += $1 - $2 } END { printf "added lines: %s, removed lines: %s, total lines: %s\n", add, subs, loc }' -; done

结果示例

Max-laptop    added lines: 1192, removed lines: 748, total lines: 444
chengshuai    added lines: 120745, removed lines: 71738, total lines: 49007
cisen    added lines: 3248, removed lines: 1719, total lines: 1529
max-h    added lines: 1002, removed lines: 473, total lines: 529
max-l    added lines: 2440, removed lines: 617, total lines: 1823
mw    added lines: 148721, removed lines: 6709, total lines: 142012
spider    added lines: 2799, removed lines: 1053, total lines: 1746
thy    added lines: 34616, removed lines: 13368, total lines: 21248
wmao    added lines: 12, removed lines: 8, total lines: 4
xrl    added lines: 10292, removed lines: 6024, total lines: 4268
yunfei.huang    added lines: 427, removed lines: 10, total lines: 417
³Ÿö    added lines: 5, removed lines: 3, total lines: 2

查看仓库提交者排名前 5

git log --pretty='%aN' | sort | uniq -c | sort -k1 -n -r | head -n 5

贡献值统计

git log --pretty='%aN' | sort -u | wc -l

提交数统计

git log --oneline | wc -l

添加或修改的代码行数:

git log --stat|perl -ne 'END { print $c } $c += $1 if /(\d+) insertions/'

使用gitstats

GitStats项目,用Python开发的一个工具,通过封装Git命令来实现统计出来代码情况并且生成可浏览的网页。官方文档可以参考这里。

使用方法

git clone git://github.com/hoxu/gitstats.git
cd gitstats
./gitstats 你的项目的位置 生成统计的文件夹位置

可能会提示没有安装gnuplot画图程序,那么需要安装再执行:

//mac osx
brew install gnuplot
//centos linux
yum install gnuplot

生成的统计文件为HTML:
2014-8-16-git.jpg

使用cloc

npm install -g cloc

image

参考文章

git代码行统计命令集
统计本地Git仓库中不同贡献者的代码行数的一些方法
使用Git工具统计代码

查看原文

刘宁Leo 发布了文章 · 2018-12-12

React中富文本编辑器的技术选型调研

前言

富文本编辑器是项目中不可或缺的部分,目前市面上可以选择的富文本编辑器种类繁多,如何在项目中选择一款集轻量,美观,稳定,坑少,满足需求的富文本编辑器变成了团队中一个重要的问题。

现在项目中使用的是 WangEditor,为了寻找到更好的替代品,我针对 Ant Design 官方推荐的两款的富文本编辑器做了调研。Ant Design 称它们为“社区精选组件”,在心理层面上觉得应该会比较稳定,尤其是结合 Ant Design 使用。

我对这两款富文本编辑器都进行了使用,并结合目前的项目需求进行了比较。下面是我的使用体验。

react-quill

第一款富文本编辑器叫作 react-quill,是国外一个社区维护的,贡献者有二三十人。quill 在英文中是鹅毛笔的意思,听起来还是很有美感的。点击这里查看 demo

基本使用

下面是 react-quill 的最基本的用法,非常简单。

import ReactQuill from 'react-quill';
import 'react-quill/dist/quill.snow.css'

class MyComponent extends React.Component {
  constructor(props) {
    super(props)
    this.state = { text: '' }
    this.handleChange = this.handleChange.bind(this)
  }

  handleChange(value) {
    this.setState({ text: value })
  }

  render() {
    return (
      <ReactQuill value={this.state.text} onChange={this.handleChange} />
    )
  }
}

自定义工具栏

核心就是配置modulesformatsmodules是配置工具栏上的内容,即决定工具栏上有什么;formats是决定哪些工具栏选项可以启用,即决定工具栏的哪些可以生效。实例代码如下:

class MyComponent extends Component {
  constructor(props) {
    super(props);
    this.state = {
      text: '',
    }
  }

  modules = {
    toolbar: [
      [{ 'header': [1, 2, false] }],
      ['bold', 'italic', 'underline','strike', 'blockquote'],
      [{'list': 'ordered'}, {'list': 'bullet'}, {'indent': '-1'}, {'indent': '+1'}],
      ['link', 'image'],
      ['clean']
    ],
  };

  formats = [
    'header',
    'bold', 'italic', 'underline', 'strike', 'blockquote',
    'list', 'bullet', 'indent',
    'link', 'image'
  ];

  render() {
    return (
      <div className="text-editor">
        <ReactQuill modules={this.modules} formats={this.formats} />
      </div>
    );
  }
}

这里还有更高级的用法,我们可以在工具栏上加入自己设计的JXS元素,但是目前的项目需求用不到,这里就不展开了。

优势

  1. 简洁美观大方。
  2. 对于用户从各种地方粘贴过来的文字兼容得很好,不管你之前带有什么奇奇怪怪的格式,统统转化成了带有<p></p>标签的的文本。
  3. 跟 Ant Design 和 React 的融入度非常高,可以很方便的放在<Form></Form>作为一个受控组件。传入的 value 就是 HTML 字符串,不需要做任何格式转换。

劣势

  1. 图片格式是转成了base64,这跟目前项目中图片上传方式不兼容。虽然这个问题有解决方法,React-Quill中的图片上传及显示
  2. 不支持 Excel 格式的数据。因为目前项目中使用的 WangEditor 支持表格数据,所以如果老数据中存在表格,那么替换后的表格数据显示将成为一个问题。

braft-editor

这是由中国人开发的个人项目,点这里查看 demo

基本使用

实例代码如下,需要注意的一点是,接收的 value 不再是 HTML 字符串了,而是editorState格式。
可以通过editorState.toHTML()得到 HTML 字符串。

import React from 'react';
import 'braft-editor/dist/index.css';
import BraftEditor from 'braft-editor';

class Braft extends React.Component {
  constructor(props) {
    super(props);
    this.state = { editorState: BraftEditor.createEditorState(null) };
  }
  handleChange = editorState => {
    this.setState({ editorStste });
  };
  render() {
    return (
      <BraftEditor value={this.state.editorStste} onChange={this.handleChange}/>
    );
  }
}

自定义工具栏

通过配置属性controls来自定义工具栏,也可以自定义工具栏图标的文字和样式。示例如下:

const controls = [
    'undo', 'redo', 'separator',
    {
        key: 'bold', // 使用key来指定控件类型
        title: '加粗选中文字哦', // 自定义控件title
        text: '点我加粗', // 使用自定义文案来代替默认图标(B),此处也可传入jsx
    },
    'italic', 'underline', 'strike-through'
]

优势

  1. 有“全屏”功能,可以全屏编辑内容,还是很炫酷的。
  2. 作者是中国人,文档清晰易读。

劣势

  1. 图片格式转成了base64,跟目前项目中图片上传方式不兼容。
  2. 不支持 Excel 格式的数据。

总结

这两款编辑器的稳定性和对异常文本的处理能力都强于 WangEditor,但是存在的问题是都对于表格数据不支持,所以对于老数据的显示存在风险。

查看原文

赞 15 收藏 13 评论 2

刘宁Leo 评论了文章 · 2018-10-22

谈谈深拷贝与浅拷贝

前言

关于深拷贝和浅拷贝其实是两个比较基础的概念,但是我还是想整理一下,因为里面有很多小细节还是很有意思的。

深拷贝和浅拷贝的区别

深拷贝和浅拷贝是大家经常听到的两个名词,两者到底有什么不同呢?

先看看什么是浅拷贝。

var obj1 = { a: 1, b: 2 };
var obj2 = obj1;
obj2.a = 3;

console.log(obj1); // { a: 3, b: 2 }
console.log(obj2); // { a: 3, b: 2 }

这是一个最简单的浅拷贝的例子,我把obj1赋值给obj2,改变了obj2中的一个属性值,obj1中的相应属性值也跟着变化了,这是为什么呢?

因为浅拷贝其实只是引用的拷贝,两者还是指向内存中的同一个地址。简而言之,就是obj1和obj2其实指向的是同一个对象。打个比方,就像一个房间把门牌号1换成了门牌号2,但是这个房间还是这个房间。

深拷贝就是两者指向不同的内存地址,是真正意义上的拷贝。拿上面的房间举例子,就是你真的重新开了一间房,并不是只是换门牌号。

谈谈Object.assign()

Object.assign()是我们经常用到的方法,其实这个方法就是浅拷贝。但是它又有一点特殊的地方,就是可以处理第一层的深拷贝。

var obj1 = { a: 1, b: { c: 2 } };
var obj2 = Object.assign({}, obj1);
obj2.a = 3;
obj2.b.c = 3;
console.log(obj1); // { a: 1, b: { c: 3 } }
console.log(obj2); // { a: 3, b: { c: 3 } }

看上面的例子,属性a的值并没有跟着变,但是属性b中的c的值跟着变了。

常用的实现深拷贝的方式

JSON

这是最常用的实现深拷贝的方式,直接看例子:

var obj1 = { a: { b: 1 } };
var obj2 = JSON.parse(JSON.stringify(obj1));

这种方法很简单而且好用,但是有一点点瑕疵,它会抛弃对象的constructor。也就是深拷贝之后,不管这个对象原来的构造函数是什么,在深拷贝之后都会变成Object。

而且这种方法能正确处理的对象只有 Number, String, Boolean, Array,即那些能够被JSON直接表示的数据结构。RegExp对象或者function是无法通过这种方式深拷贝。

lodash

这是我个人目前使用的方法,只需要一行var obj2 = _.cloneDeep(obj1)就能实现。而且lodash是一个功能很强大的库,提供的方法可靠又简单,真的是懒人必备,点击这里去了解它吧!

查看原文

刘宁Leo 发布了文章 · 2018-10-19

谈谈深拷贝与浅拷贝

前言

关于深拷贝和浅拷贝其实是两个比较基础的概念,但是我还是想整理一下,因为里面有很多小细节还是很有意思的。

深拷贝和浅拷贝的区别

深拷贝和浅拷贝是大家经常听到的两个名词,两者到底有什么不同呢?

先看看什么是浅拷贝。

var obj1 = { a: 1, b: 2 };
var obj2 = obj1;
obj2.a = 3;

console.log(obj1); // { a: 3, b: 2 }
console.log(obj2); // { a: 3, b: 2 }

这是一个最简单的浅拷贝的例子,我把obj1赋值给obj2,改变了obj2中的一个属性值,obj1中的相应属性值也跟着变化了,这是为什么呢?

因为浅拷贝其实只是引用的拷贝,两者还是指向内存中的同一个地址。简而言之,就是obj1和obj2其实指向的是同一个对象。打个比方,就像一个房间把门牌号1换成了门牌号2,但是这个房间还是这个房间。

深拷贝就是两者指向不同的内存地址,是真正意义上的拷贝。拿上面的房间举例子,就是你真的重新开了一间房,并不是只是换门牌号。

谈谈Object.assign()

Object.assign()是我们经常用到的方法,其实这个方法就是浅拷贝。但是它又有一点特殊的地方,就是可以处理第一层的深拷贝。

var obj1 = { a: 1, b: { c: 2 } };
var obj2 = Object.assign({}, obj1);
obj2.a = 3;
obj2.b.c = 3;
console.log(obj1); // { a: 1, b: { c: 3 } }
console.log(obj2); // { a: 3, b: { c: 3 } }

看上面的例子,属性a的值并没有跟着变,但是属性b中的c的值跟着变了。

常用的实现深拷贝的方式

JSON

这是最常用的实现深拷贝的方式,直接看例子:

var obj1 = { a: { b: 1 } };
var obj2 = JSON.parse(JSON.stringify(obj1));

这种方法很简单而且好用,但是有一点点瑕疵,它会抛弃对象的constructor。也就是深拷贝之后,不管这个对象原来的构造函数是什么,在深拷贝之后都会变成Object。

而且这种方法能正确处理的对象只有 Number, String, Boolean, Array,即那些能够被JSON直接表示的数据结构。RegExp对象或者function是无法通过这种方式深拷贝。

lodash

这是我个人目前使用的方法,只需要一行var obj2 = _.cloneDeep(obj1)就能实现。而且lodash是一个功能很强大的库,提供的方法可靠又简单,真的是懒人必备,点击这里去了解它吧!

查看原文

赞 21 收藏 17 评论 3

刘宁Leo 发布了文章 · 2018-10-09

走进机器学习世界之TensorFlow.js快速上手

前言

近两年人工智能,机器学习等各种概念漫天飞舞,那人工智能,机器学习,深度学习这些名词之间是什么关系呢?

如果用三个同心圆来解释的话,人工智能是最大的圆,机器学习是中间的圆,深度学习是最小的圆。具体解释就是:

  • 机器学习是实现人工智能的一种手段
  • 深度学习是实现机器学习的一种技术

今天我们要介绍的TensorFlow.js是由Google的AI团队发布一款机器学习框架,基于DeepLearn.js(已经停止更新)。这款机器学习框架的特点是使用JavaScript语言,在浏览器中就可以使用它提供的各种API来进行建模和训练,并且支持Node.js。所以对于前端来说,是走进机器学习世界最便捷的路径了。

这里有一个利用TensorFlow.js实现的机器学习的小游戏demo,大家可以感受一下。尝试一下

这篇文章基于TensorFlow.js的英文官方文档写成,重点在于TensorFlow.js的入门,关于机器学习更多的知识点可参考Google机器学习课程

让我们开始吧!

安装

直接引入

第一种方式是通过<script></script>直接引入,在浏览器中运行下面的代码,在控制台中可以看到结果。

<html>
  <head>
    <!-- Load TensorFlow.js -->
    <script data-original="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@0.13.0"> </script>

    <!-- Place your code in the script tag below. You can also use an external .js file -->
    <script>
      // Notice there is no 'import' statement. 'tf' is available on the index-page
      // because of the script tag above.

      // Define a model for linear regression.
      const model = tf.sequential();
      model.add(tf.layers.dense({units: 1, inputShape: [1]}));

      // Prepare the model for training: Specify the loss and the optimizer.
      model.compile({loss: 'meanSquaredError', optimizer: 'sgd'});

      // Generate some synthetic data for training.
      const xs = tf.tensor2d([1, 2, 3, 4], [4, 1]);
      const ys = tf.tensor2d([1, 3, 5, 7], [4, 1]);

      // Train the model using the data.
      model.fit(xs, ys, {epochs: 10}).then(() => {
        // Use the model to do inference on a data point the model hasn't seen before:
        // Open the browser devtools to see the output
        model.predict(tf.tensor2d([5], [1, 1])).print();
      });
    </script>
  </head>

  <body>
  </body>
</html>

npm或yarn

第二种方式是通过npm或yarn将TensorFlow.js的库引入到你的项目中。

yarn add @tensorflow/tfjs  
npm install @tensorflow/tfjs

你可以在你的main.js中添加如下代码:

import * as tf from '@tensorflow/tfjs';

// Define a model for linear regression.
const model = tf.sequential();
model.add(tf.layers.dense({units: 1, inputShape: [1]}));

// Prepare the model for training: Specify the loss and the optimizer.
model.compile({loss: 'meanSquaredError', optimizer: 'sgd'});

// Generate some synthetic data for training.
const xs = tf.tensor2d([1, 2, 3, 4], [4, 1]);
const ys = tf.tensor2d([1, 3, 5, 7], [4, 1]);

// Train the model using the data.
model.fit(xs, ys, {epochs: 10}).then(() => {
  // Use the model to do inference on a data point the model hasn't seen before:
  model.predict(tf.tensor2d([5], [1, 1])).print();
});

如果不懂上面代码的含义不要着急,继续看后面的一些基础概念和用法。

Tensor和Variable

Tensor和Variable是TensorFlow.js中最基础的两种数据形式。那他们到底是什么意思呢?

Tensor在谷歌翻译中是“张量”的意思,“张量”这个词是数学和物理中的一个术语,我们暂且不深究它的意思,你只需要记住,Tensor(张量)是不可变的,类似于const,一旦定义就不能改变它的值。

Variable就很容易理解了,它是变量的意思,顾名思义,它的值是可以改变的。

总之,Tensor(张量)不可变,Variable(变量)可变。

Tensor

张量通常是一个0到多维的数组,构造张量时会用到shape属性,用来规定这是一个几行几列的数组。
请看下面构造一个张量的例子。shape用来规定这个张量是两行三列的数组,然后可以看到最后的输出,我们得到了一个两行三列的二维数组。

// 2x3 Tensor
const shape = [2, 3]; // 2 rows, 3 columns
const a = tf.tensor([1.0, 2.0, 3.0, 10.0, 20.0, 30.0], shape);
a.print(); // print Tensor values
// Output: [[1 , 2 , 3 ],
//          [10, 20, 30]]

也可以用下面这种方式,直接表示这是一个两行三列的二维数组。

// The shape can also be inferred:
const b = tf.tensor([[1.0, 2.0, 3.0], [10.0, 20.0, 30.0]]);
b.print();
// Output: [[1 , 2 , 3 ],
//          [10, 20, 30]]

然而实际上,我们通常使用 tf.scalar, tf.tensor1d, tf.tensor2d, tf.tensor3d 和 tf.tensor4d来构造张量。tf.scalar是构造一个零维数组,也就是一个数字,tf.tensor1d是构造一位数组,tf.tensor2d是构造二维数组,以此类推。例如:

const c = tf.tensor2d([[1.0, 2.0, 3.0], [10.0, 20.0, 30.0]]);
c.print();
// Output: [[1 , 2 , 3 ],
//          [10, 20, 30]]

或者使用tf.zeros生成全是0的数组,tf.ones生成全是1的数组,例如:

// 3x5 Tensor with all values set to 0
const zeros = tf.zeros([3, 5]);
// Output: [[0, 0, 0, 0, 0],
//          [0, 0, 0, 0, 0],
//          [0, 0, 0, 0, 0]]

Variable

而Variable(变量)只能通过Tensor(张量)生成。我们可以使用assign给变量重新赋值。例如:

const initialValues = tf.zeros([5]);
const biases = tf.variable(initialValues); // initialize biases
biases.print(); // output: [0, 0, 0, 0, 0]

const updatedValues = tf.tensor1d([0, 1, 0, 1, 0]);
biases.assign(updatedValues); // update values of biases
biases.print(); // output: [0, 1, 0, 1, 0]

Operations

TensorFlow.js提供了各种向量运算的API,我们可以称这些为Operations。下面是张量平方和张量相加的例子:

const d = tf.tensor2d([[1.0, 2.0], [3.0, 4.0]]);
const d_squared = d.square();
d_squared.print();
// Output: [[1, 4 ],
//          [9, 16]]

const e = tf.tensor2d([[1.0, 2.0], [3.0, 4.0]]);
const f = tf.tensor2d([[5.0, 6.0], [7.0, 8.0]]);

const e_plus_f = e.add(f);
e_plus_f.print();
// Output: [[6 , 8 ],
//          [10, 12]]

而且TensorFlow.js还提供了链式运算,请看例子:

const sq_sum = e.add(f).square();
sq_sum.print();
// Output: [[36 , 64 ],
//          [100, 144]]

// All operations are also exposed as functions in the main namespace,
// so you could also do the following:
const sq_sum = tf.square(tf.add(e, f));

Model

上面我们介绍了张量,变量和一些基础运算,下面我们引入“Model(模型)”这个概念。

模型就是一个函数,给定这个函数特定的输入,会返回特定的输出。

所以请记住,模型就是一个函数而已。

我们来看一个定义模型的例子, 以下代码构造了一个 y = a x ^ 2 + b x + c 的函数表达式,给定一个x,我们会得到一个y。

代码中tf.tidy()看不懂请忽略,我们将在下一节介绍,它只是用来清除内存。

// Define function
function predict(input) {
  // y = a * x ^ 2 + b * x + c
  // More on tf.tidy in the next section
  return tf.tidy(() => {
    const x = tf.scalar(input);

    const ax2 = a.mul(x.square());
    const bx = b.mul(x);
    const y = ax2.add(bx).add(c);

    return y;
  });
}

// Define constants: y = 2x^2 + 4x + 8
const a = tf.scalar(2);
const b = tf.scalar(4);
const c = tf.scalar(8);

// Predict output for input of 2
const result = predict(2);
result.print() // Output: 24

但是通常,我们会使用一个更高级的API去构造模型,那就是用 tf.model 的形式,这里的model只是模型的总称,并没有 tf.modal 这个方法。TensorFlow中最常用的是 tf.sequential,例如:

const model = tf.sequential();
model.add(
  tf.layers.simpleRNN({
    units: 20,
    recurrentInitializer: 'GlorotNormal',
    inputShape: [80, 4]
  })
);

const optimizer = tf.train.sgd(LEARNING_RATE);
model.compile({optimizer, loss: 'categoricalCrossentropy'});
model.fit({x: data, y: labels});

上面代码中一定有很多你不理解的地方,比如什么是 tf.layer?什么是 tf.train.sgd?这里可以先忽略细节,先从总体上体会这些基本概念,关于 tf.train.sg 等我们在后面的文章介绍。如果你忍不住,就自己去查吧!给你 官方API文档 好了。

内存管理

TensorFlow.js使用GPU来加速运算,所以合理地释放内存是一件很必要的事情。TensorFlow.js提供了dispose函数来释放内存,请看例子:

const x = tf.tensor2d([[0.0, 2.0], [4.0, 6.0]]);
const x_squared = x.square();

x.dispose();
x_squared.dispose();

但是通常实际中我们会面对很多的张量和操作,这时候 tf.tidy 更加方便,因为它是批量释放内存,请看例子:

// tf.tidy takes a function to tidy up after
const average = tf.tidy(() => {
  // tf.tidy will clean up all the GPU memory used by tensors inside
  // this function, other than the tensor that is returned.
  //
  // Even in a short sequence of operations like the one below, a number
  // of intermediate tensors get created. So it is a good practice to
  // put your math ops in a tidy!
  const y = tf.tensor1d([1.0, 2.0, 3.0, 4.0]);
  const z = tf.ones([4]);

  return y.sub(z).square().mean();
});

average.print() // Output: 3.5

使用 tf.tidy 有两个要点:

  • 传递给 tf.tidy 的函数必须是同步的。
  • tf.tidy 不会清理变量,你只能通过 dispose 手动清理。

总结

关于 TensorFlow.js 的基础概念介绍完了,但是这只是我们探索机器学习的一个工具而已,具体的实践还需要更多的学习,后面有时间我也会跟大家一起学习,并及时分享。

查看原文

赞 9 收藏 7 评论 1

刘宁Leo 评论了文章 · 2018-10-08

几张图看明白MVC MVP MVVM

前言

这篇文章整理于阮一峰老师的文章,我整理过来做一下学习记录,也方便有需要的朋友浏览。

MVC

MVC由以下三部分组成:

图片描述

  1. 视图(View):用户界面
  2. 控制器(Controller):业务逻辑
  3. 模型(Model):数据保存

各部分之间的通信过程如下:

图片描述

  1. View 传送指令到 Controller
  2. Controller 完成业务逻辑后,要求 Model 改变状态
  3. Model 将新的数据发送到 View,用户得到反馈

用户进行操作时,MVC可以分成两种方式。一种是通过 View 接受指令,传递给 Controller:

图片描述

另一种是直接通过controller接受指令:

图片描述

MVP

MVP 模式将 Controller 改名为 Presenter,同时改变了通信方向:

图片描述

  1. 各部分之间的通信,都是双向的
  2. View 与 Model 不发生联系,都通过 Presenter 传递
  3. View 非常薄,不部署任何业务逻辑,称为被动视图(Passive View),即没有任何主动性,而 Presenter非常厚,所有逻辑都部署在那里

MVVM

MVVM 模式将 Presenter 改名为 ViewModel,基本上与 MVP 模式完全一致,唯一的区别是,它采用双向绑定(data-binding),View的变动,自动反映在 ViewModel,反之亦然:

图片描述

查看原文

刘宁Leo 收藏了文章 · 2018-08-27

从零搭建Koa2 Server

前几天想写个小爬虫程序,准备后端就用koa2。于是翻遍github与各大网站,都没找到一个好用的、轻一点的koa2脚手架,也找不到一个清晰些的搭建介绍。github上的脚手架要么是1.x版的koa,要么一堆复杂的依赖。

当然可能还是有写的比较好的吧,只是我没找到。不管怎样吧,我只能亲自上了,就当是学习了。

现在把搭建过程介绍下,看能不能方便下入门的同学。

第一步:初始项目,引入 Koa2

官方的介绍,是很简单的。

$ npm install koa
const Koa = require('koa')
const app = new Koa()

// response
app.use(ctx => {
  ctx.body = 'Hello Koa'
})

app.listen(3000)

好,那我们就先从这开始。创建一个文件夹,命名koa2。(记得先装好node v7.6.0 以上版本)

cd koa2

npm init // 一路回车,根据提示输入信息。

npm install koa --save

然后在文件下根目录下创建程序入口文件:index.js,并把官网介绍那段代码贴进去。之后在命令行中执行

node index.js

打开浏览器,访问 http://localhost:3000/ ,可以看到页面输出了 hello world

很好,第一步已经踏出去了。相信到这里大部分小白都没问题,之后就开始懵逼了。就这个玩意,我该怎么写接口?怎么连接数据库?

第二步:搭建路由与Controller

Koa本质上是调用一系列的中间件,来处理对应的请求,并决定是否传递到下一个中间件去处理。我们来写一个最简单的中间件试试。

// 刚才index.js 中的这段代码,我们改写一下。
app.use(ctx => {
  ctx.body = 'Hello Koa'
})

// 改成如下

app.use(ctx => {
  ctx.body = `您的网址路径为:${ctx.request.url}`
})

这段代码中,app.usefunction 就是最简单的一个中间件,接受了请求,读出请求路径,并返回到客户端。重新执行下node index.js,打开浏览器,输入http://localhost:3000/hhhhh,页面输出了您的网址路径为:hhhhh

所以,接口的本质,就是判断不同的请求链接,干不同的事情,返回相应的结果。那么我们得需要一个路由中间件来处理分发请求。开源的时代,当然是拿来主义了。github搜下koa-router,成功找到。根据它的介绍,我们先在项目根目录执行

npm install koa-router --save

然后把index.js文件再改造下。变成如下:

const Koa = require('koa')
const Router = require('koa-router')
const app = new Koa()
const router = new Router()

// 先注释了,后面再解释
// const bodyParser = require('koa-bodyparser')
// app.use(bodyParser())

router.get('/', ctx => {
  ctx.body = `这是主页`
})

router.get('/user', ctx => {
  ctx.body = `这是user页`
})

router.get('/post', ctx => {
  ctx.body = ctx.request.body
})

router.get('/async', async ctx => {
  const sleep = async (ms) => {
    return new Promise(resolve => {
      setTimeout(() => {
        resolve(true)
      }, ms)
    })
  }
  await sleep(1000)
  ctx.body = `这是异步处理页`
})

app
  .use(router.routes())
  .use(router.allowedMethods())

app.listen(3000)

重新执行 node index.js。我们可以看到访问 /, /user/async,都能得到相应的结果了。

除了那个post的方法,压根得不到自己post的数据。

因为koa是很纯粹的,你提交的数据,它并不会帮你处理。所以这里我们又必须引用一个中间件来处理提交的数据--bodyparser。把上面那两行注释代码解注,就能处理请求数据了。记得要先

npm install koa-bodyparser --save

另外关于async/await不明白的同学,可以先去看下阮老师的介绍,点击传送门

不过我们不能把所有的接口都写在这一个文件呀,所以我们得改造下。理一下思路,路由的配置文件应该单独一份,接口的方法应该按业务模块分成一个个controller。说干就干!

先看改造后的目录结构,不想截图,大家将就看看:

-koa2
  -node_modules
  -controller
    user.js
  -index.js
  -router.js
  -package.json

再来看文件变成怎么样了。

// index.js

const Koa = require('koa')
const app = new Koa()
const router = require('./router')
const bodyParser = require('koa-bodyparser')

app.use(bodyParser())

app
  .use(router.routes())
  .use(router.allowedMethods())

app.listen(3000)
// router.js

const Router = require('koa-router')
const router = new Router()
const user = require('./controller/user')

router.post('/user/login', user.login)
router.get('/user/profile', user.profile)

module.exports = router
// controller/user.js

const sleep = async (ms) => {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(true)
    }, ms)
  })
}
module.exports = {
  login (ctx) {
    ctx.body = {
      username: ctx.request.body.username
    }
  },
  async profile (ctx) {
    await sleep(1000)
    ctx.body = {
      username: '相学长',
      sex: 'man',
      age: '999'
    }
  }
}

再重新执行 node index.js。访问相应路由,应该能得到对应的结果了。

其他工程化配置

好,到此为止,我们的server已经大致完成了,但是我们发现一个很烦的问题就是,每次修改代码都得重新node index.js,这也太烦了。我希望的是,每次更新代码都能重新执行,并且帮我执行ESlint。其他前端项目webpack那一套,不是webpack配置工程师的话,自己挪过来又改不来。

这里我介绍个简单的方案,nodemon + gulp。具体呢就不一步步来了,这种东西,不用太了解,能run起来满足自己需求就好。如果不需要eslint的话,只要安装nodemon就好。

package.json scripts部分 修改为:

"scripts": {
  "nodemon": "nodemon index.js"
}

然后命令行执行:

npm install nodemon --save-dev

npm run nodemon

如果有eslint的需求的话,就稍微麻烦些了,eslint的init我就不贴教程了,我贴上我的gulp配置文件:

// gulpfile.js
const gulp = require('gulp')
const lint = require('gulp-eslint')
const nodemon = require('gulp-nodemon')

function lintFiles (files) {
  return gulp.src(files)
    .pipe(lint())
    .pipe(lint.format())
    // .pipe(lint.failAfterError())
}

gulp.task('eslint', () => lintFiles(['**/*.js', '!node_modules/**']))

gulp.task('eslint_nodemon', ['eslint'], () => {
  return nodemon({
    script: './app/server.js', // 项目入口文件
    tasks (changedFiles) {
      lintFiles(changedFiles)
      return []
    },
    ignore: ['build/**', 'dist/**', '.git', 'node_modules/**']
  })
})

gulp.task('default', ['eslint_nodemon'])
// package.json scripts
"scripts": {
  "start": "pm2 start index.js --watch", // 这里用pm2 作为线上run,有兴趣的同学可以自己去看看
  "dev": "gulp",
  "lint": "eslint .",
  "fix": "eslint --fix ."
},

写在最后

到这里,我想应该能让一部分同学上手了。

但这只是初步的搭建了下koa。真的想投入使用,根据业务需求,可能还需要安装数据库驱动等中间件。对于复杂业务场景的server,还需要更加合理的设计controller,service,在这里就不多阐述了。

如果这篇文章,能够帮助到一些同学,下次有空再写写这方面相关的。

ps: 文章介绍的所有代码都传了一份到github,有需要的同学请自行去看。

地址:https://github.com/wuomzfx/koa2

查看原文

刘宁Leo 收藏了文章 · 2018-08-24

Node+Koa2+Mysql 搭建简易博客

Koa2-blog

2018-1-5 更新教程(新增上传头像、新增分页、样式改版、发布文章和评论支持markdown语法)
现在GitHub的代码结构有变现在GitHub的代码结构有变,接口名也有变动。

Node+Koa2+Mysql 搭建简易博客

预览地址

http://blog.wclimb.site

写在前面

本篇教程一方面是为了自己在学习的过程加深记忆,也是总结的过程。另一方面给对这方面不太了解的同学以借鉴经验。如发现问题还望指正,
如果你觉得这篇文章帮助到了你,那就赏脸给个star吧,https://github.com/wclimb/Koa...
下一篇可能是 Node + express + mongoose 或 zepto源码系列
感谢您的阅读^_^
ps:关于markdown代码缩进问题,看起来不太舒服,但复制到编辑器是正常的哟!

演示效果

img

开发环境

  • nodejs v8.1.0
  • koa v2.3.0
  • mysql v5.7.0

准备工作

文中用到了promise、async await等语法,所以你需要一点es6的语法,传送门当然是阮老师的教程了 http://es6.ruanyifeng.com/

如果你已经配置好node和mysql可以跳过

经常会有人问报错的问题,运行出错大部分是因为不支持async,升级node版本可解决
$ node -v   查看你的node版本,如果过低则去nodejs官网下载替换之前的版本

下载mysql,并设置好用户名和密码,默认可以为用户名:root,密码:123456

进入到 bin 目录下 比如 cd C:\Program Files\MySQL\MySQL Server 5.7\bin

然后开启mysql

$ mysql -u root -p    

输入密码之后创建database(数据库),nodesql是我们创建的数据库

$ create database nodesql;

记住sql语句后面一定要跟;符号,接下来看看我们创建好的数据库列表

$ show databases;

img

启用创建的数据库

$ use nodesql;

查看数据库中的表

$ show tables;

显示Empty set (0.00 sec),因为我们还没有建表,稍后会用代码建表
注释:
这是后面建表之后的状态
img

目录结构

img

  • config 存放默认文件
  • lib 存放操作数据库文件
  • middlewares 存放判断登录与否文件
  • public 存放样式和头像文件
  • routes 存放路由文件
  • views 存放模板文件
  • index 程序主文件
  • package.json 包括项目名、作者、依赖等等

首先我们创建koa2-blog文件夹,然后cd koa2-blog

 接着使用 npm init 来创建package.json

接着安装包,安装之前我们使用cnpm安装

$ npm install -g cnpm --registry=https://registry.npm.taobao.org
$ cnpm i koa koa-bodyparser koa-mysql-session koa-router koa-session-minimal koa-static koa-views md5 moment mysql ejs markdown-it chai mocha koa-static-cache --save-dev

各模块用处

  1. koa node框架
  2. koa-bodyparser 表单解析中间件
  3. koa-mysql-sessionkoa-session-minimal 处理数据库的中间件
  4. koa-router 路由中间件
  5. koa-static 静态资源加载中间件
  6. ejs 模板引擎
  7. md5 密码加密
  8. moment 时间中间件
  9. mysql 数据库
  10. markdown-it markdown语法
  11. koa-views 模板呈现中间件
  12. chaimocha 测试使用
  13. koa-static-cache 文件缓存

在文件夹里面新建所需文件

img

首先配置config

我们新建default.js文件

const config = {
  // 启动端口
  port: 3000,

  // 数据库配置
  database: {
    DATABASE: 'nodesql',
    USERNAME: 'root',
    PASSWORD: '123456',
    PORT: '3306',
    HOST: 'localhost'
  }
}

module.exports = config

这是我们所需的一些字段,包括端口和数据库连接所需,最后我们把它exports暴露出去,以便可以在别的地方使用

配置index.js文件

index.js

const Koa = require('koa');
const path = require('path')
const bodyParser = require('koa-bodyparser');
const ejs = require('ejs');
const session = require('koa-session-minimal');
const MysqlStore = require('koa-mysql-session');
const config = require('./config/default.js');
const router=require('koa-router')
const views = require('koa-views')
// const koaStatic = require('koa-static')
const staticCache = require('koa-static-cache')
const app = new Koa()


// session存储配置
const sessionMysqlConfig = {
  user: config.database.USERNAME,
  password: config.database.PASSWORD,
  database: config.database.DATABASE,
  host: config.database.HOST,
}

// 配置session中间件
app.use(session({
  key: 'USER_SID',
  store: new MysqlStore(sessionMysqlConfig)
}))


// 配置静态资源加载中间件
// app.use(koaStatic(
//   path.join(__dirname , './public')
// ))
// 缓存
app.use(staticCache(path.join(__dirname, './public'), { dynamic: true }, {
  maxAge: 365 * 24 * 60 * 60
}))
app.use(staticCache(path.join(__dirname, './images'), { dynamic: true }, {
  maxAge: 365 * 24 * 60 * 60
}))

// 配置服务端模板渲染引擎中间件
app.use(views(path.join(__dirname, './views'), {
  extension: 'ejs'
}))
app.use(bodyParser({
  formLimit: '1mb'
}))

//  路由(我们先注释三个,等后面添加好了再取消注释,因为我们还没有定义路由,稍后会先实现注册)
//app.use(require('./routers/signin.js').routes())
app.use(require('./routers/signup.js').routes())
//app.use(require('./routers/posts.js').routes())
//app.use(require('./routers/signout.js').routes())


app.listen(3000)

console.log(`listening on port ${config.port}`)

我们使用koa-session-minimal`koa-mysql-session`来进行数据库的操作
使用koa-static配置静态资源,目录设置为public
使用ejs模板引擎
使用koa-bodyparser来解析提交的表单信息
使用koa-router做路由
使用koa-static-cache来缓存文件
之前我们配置了default.js,我们就可以在这里使用了
首先引入进来 var config = require('./config/default.js');
然后在数据库的操作的时候,如config.database.USERNAME,得到的就是root。

如果你觉得这篇文章帮助到了你,那就赏脸给个star吧,https://github.com/wclimb/Koa...

配置lib的mysql.js文件

关于数据库的使用这里介绍一下,首先我们建立了数据库的连接池,以便后面的操作都可以使用到,我们创建了一个函数query,通过返回promise的方式以便可以方便用.then()来获取数据库返回的数据,然后我们定义了三个表的字段,通过createTable来创建我们后面所需的三个表,包括posts(存储文章),users(存储用户),comment(存储评论),create table if not exists users()表示如果users表不存在则创建该表,避免每次重复建表报错的情况。后面我们定义了一系列的方法,最后把他们exports暴露出去。

这里只介绍注册用户insertData,后续的可以自行查看,都差不多
// 注册用户
let insertData = function( value ) {
  let _sql = "insert into users set name=?,pass=?,avator=?,moment=?;"
  return query( _sql, value )
}

我们写了一个_sql的sql语句,意思是插入到users的表中(在这之前我们已经建立了users表)然后要插入的数据分别是name、pass、avator、moment,就是用户名、密码、头像、注册时间,最后调用query函数把sql语句传进去(之前的写法是"insert into users(name,pass) values(?,?);",换成现在得更容易理解)

lib/mysql.js

var mysql = require('mysql');
var config = require('../config/default.js')

var pool  = mysql.createPool({
  host     : config.database.HOST,
  user     : config.database.USERNAME,
  password : config.database.PASSWORD,
  database : config.database.DATABASE
});

let query = function( sql, values ) {

  return new Promise(( resolve, reject ) => {
    pool.getConnection(function(err, connection) {
      if (err) {
        reject( err )
      } else {
        connection.query(sql, values, ( err, rows) => {

          if ( err ) {
            reject( err )
          } else {
            resolve( rows )
          }
          connection.release()
        })
      }
    })
  })

}


// let query = function( sql, values ) {
// pool.getConnection(function(err, connection) {
//   // 使用连接
//   connection.query( sql,values, function(err, rows) {
//     // 使用连接执行查询
//     console.log(rows)
//     connection.release();
//     //连接不再使用,返回到连接池
//   });
// });
// }

let users =
    `create table if not exists users(
     id INT NOT NULL AUTO_INCREMENT,
     name VARCHAR(100) NOT NULL,
     pass VARCHAR(100) NOT NULL,
     avator VARCHAR(100) NOT NULL,
     moment VARCHAR(100) NOT NULL,
     PRIMARY KEY ( id )
    );`

let posts =
    `create table if not exists posts(
     id INT NOT NULL AUTO_INCREMENT,
     name VARCHAR(100) NOT NULL,
     title TEXT(0) NOT NULL,
     content TEXT(0) NOT NULL,
     md TEXT(0) NOT NULL,
     uid VARCHAR(40) NOT NULL,
     moment VARCHAR(100) NOT NULL,
     comments VARCHAR(200) NOT NULL DEFAULT '0',
     pv VARCHAR(40) NOT NULL DEFAULT '0',
     avator VARCHAR(100) NOT NULL,
     PRIMARY KEY ( id )
    );`

let comment =
    `create table if not exists comment(
     id INT NOT NULL AUTO_INCREMENT,
     name VARCHAR(100) NOT NULL,
     content TEXT(0) NOT NULL,
     moment VARCHAR(40) NOT NULL,
     postid VARCHAR(40) NOT NULL,
     avator VARCHAR(100) NOT NULL,
     PRIMARY KEY ( id )
    );`

let createTable = function( sql ) {
  return query( sql, [] )
}

// 建表
createTable(users)
createTable(posts)
createTable(comment)

// 注册用户
let insertData = function( value ) {
  let _sql = "insert into users set name=?,pass=?,avator=?,moment=?;"
  return query( _sql, value )
}
// 删除用户
let deleteUserData = function( name ) {
  let _sql = `delete from users where name="${name}";`
  return query( _sql )
}
// 查找用户
let findUserData = function( name ) {
  let _sql = `select * from users where name="${name}";`
  return query( _sql )
}
// 发表文章
let insertPost = function( value ) {
  let _sql = "insert into posts set name=?,title=?,content=?,md=?,uid=?,moment=?,avator=?;"
  return query( _sql, value )
}
// 更新文章评论数
let updatePostComment = function( value ) {
  let _sql = "update posts set comments=? where id=?"
  return query( _sql, value )
}

// 更新浏览数
let updatePostPv = function( value ) {
  let _sql = "update posts set pv=? where id=?"
  return query( _sql, value )
}

// 发表评论
let insertComment = function( value ) {
  let _sql = "insert into comment set name=?,content=?,moment=?,postid=?,avator=?;"
  return query( _sql, value )
}
// 通过名字查找用户
let findDataByName = function ( name ) {
  let _sql = `select * from users where name="${name}";`
  return query( _sql)
}
// 通过文章的名字查找用户
let findDataByUser = function ( name ) {
  let _sql = `select * from posts where name="${name}";`
  return query( _sql)
}
// 通过文章id查找
let findDataById = function ( id ) {
  let _sql = `select * from posts where id="${id}";`
  return query( _sql)
}
// 通过评论id查找
let findCommentById = function ( id ) {
  let _sql = `select * FROM comment where postid="${id}";`
  return query( _sql)
}

// 查询所有文章
let findAllPost = function () {
  let _sql = ` select * FROM posts;`
  return query( _sql)
}
// 查询分页文章
let findPostByPage = function (page) {
  let _sql = ` select * FROM posts limit ${(page-1)*10},10;`
  return query( _sql)
}
// 查询个人分页文章
let findPostByUserPage = function (name,page) {
  let _sql = ` select * FROM posts where name="${name}" order by id desc limit ${(page-1)*10},10 ;`
  return query( _sql)
}
// 更新修改文章
let updatePost = function(values){
  let _sql = `update posts set  title=?,content=?,md=? where id=?`
  return query(_sql,values)
}
// 删除文章
let deletePost = function(id){
  let _sql = `delete from posts where id = ${id}`
  return query(_sql)
}
// 删除评论
let deleteComment = function(id){
  let _sql = `delete from comment where id=${id}`
  return query(_sql)
}
// 删除所有评论
let deleteAllPostComment = function(id){
  let _sql = `delete from comment where postid=${id}`
  return query(_sql)
}
// 查找评论数
let findCommentLength = function(id){
  let _sql = `select content from comment where postid in (select id from posts where id=${id})`
  return query(_sql)
}

// 滚动无限加载数据
let findPageById = function(page){
  let _sql = `select * from posts limit ${(page-1)*5},5;`
  return query(_sql)
}
// 评论分页
let findCommentByPage = function(page,postId){
  let _sql = `select * from comment where postid=${postId} order by id desc limit ${(page-1)*10},10;`
  return query(_sql)
}

module.exports = {
    query,
    createTable,
    insertData,
      deleteUserData,
      findUserData,
    findDataByName,
      insertPost,
      findAllPost,
      findPostByPage,
    findPostByUserPage,
    findDataByUser,
    findDataById,
    insertComment,
    findCommentById,
    updatePost,
    deletePost,
    deleteComment,
    findCommentLength,
    updatePostComment,
    deleteAllPostComment,
    updatePostPv,
    findPageById,
    findCommentByPage
}

下面是我们建的表

users  posts  comment
  id    id    id  
  name    name    name  
  pass    title    content  
 avator   content      moment  
  moment   md      postid 
   -  uid      avator  
   -  moment      - 
    - comments      -    
    - pv      -     
    -  avator       -  
  • id主键递增
  • name: 用户名
  • pass:密码
  • avator:头像
  • title:文章标题
  • content:文章内容和评论
  • md:markdown语法
  • uid:发表文章的用户id
  • moment:创建时间
  • comments:文章评论数
  • pv:文章浏览数
  • postid:文章id

现在感觉有点枯燥,那我们先来实现一下注册吧

实现注册页面

routers/singup.js

const router = require('koa-router')();
const userModel = require('../lib/mysql.js');
const md5 = require('md5')
const checkNotLogin = require('../middlewares/check.js').checkNotLogin
const checkLogin = require('../middlewares/check.js').checkLogin
const moment = require('moment');
const fs = require('fs')
// 注册页面
router.get('/signup', async(ctx, next) => {
    await checkNotLogin(ctx)
    await ctx.render('signup', {
        session: ctx.session,
    })
})
    
module.exports = router

使用get方式得到'/signup'页面,然后渲染signup模板,这里我们还没有在写signup.ejs

views/signup.ejs

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>注册</title>
</head>
<body>
    <div class="container">
        <form class="form create" method="post">
            <div>
                <label>用户名:</label> 
                <input placeholder="请输入用户名" type="text" name="name">
            </div>
            <div>
                <label>密码:</label> 
                <input placeholder="请输入密码" class="password" type="password" name="password">
            </div>
            <div>
                <label>重复密码:</label> 
                <input placeholder="请确认密码" class="repeatpass" type="password" name="repeatpass">
            </div>
            <div>
                <label>上传头像:</label>
                <input type="file" name="avator" id="avator">
                <input type="hidden" id="avatorVal">
                <img class="preview" alt="预览头像">
            </div>
            <div class="submit">注册</div>
        </form>
    </div>
</body>
</html>

我们先安装supervisor

$ cnpm i supervisor -g

supervisor的作用是会监听文件的变化,而我们修改文件之后不必去重启程序

supervisor --harmony index

现在访问 localhost:3000/signup 看看效果吧。注意数据库一定要是开启的状态,不能关闭

完善注册功能

首先我们来完善一下样式吧,稍微美化一下

public/index.css

body,
header,
ul,
li,
p,
div,
html,
span,
h3,
a,
blockquote {
    margin: 0;
    padding: 0;
    color: #333;
}

body {
    margin-bottom: 20px;
}
ul,li{
    list-style-type: none;
}
a {
    text-decoration: none;
}

header {
    width: 60%;
    margin: 20px auto;
}
header:after{
    content: '';
    clear: both;
    display: table;
}
header .user_right{
    float: right
}
header .user_right .active{
    color: #5FB878;
    background: #fff;
    border: 1px solid #5FB878;
    box-shadow: 0 5px 5px #ccc;
}
header .user_name {
    float: left
}
.user_name {
    font-size: 20px;
}

.has_user a,
.has_user span,
.none_user a {
    padding: 5px 15px;
    background: #5FB878;
    border-radius: 15px;
    color: #fff;
    cursor: pointer;
    border: 1px solid #fff;
    transition: all 0.3s;
}

.has_user a:hover,.has_user span:hover{
    color: #5FB878;
    background: #fff;
    border: 1px solid #5FB878;
    box-shadow: 0 5px 5px #ccc;
}

.posts{
    border-radius: 4px; 
    border: 1px solid #ddd;
}
.posts > li{
    padding: 10px;
    position: relative;
    padding-bottom: 40px;
}
.posts .comment_pv{
    position: absolute;
    bottom: 5px;
    right: 10px;
}
.posts .author{
    position: absolute;
    left: 10px;
    bottom: 5px;
}
.posts .author span{
    margin-right: 5px;
}
.posts > li:hover {
    background: #f2f2f2;
}
.posts > li:hover pre{
    border: 1px solid #666;
}
.posts > li:hover .content{
    border-top: 1px solid #fff;
    border-bottom: 1px solid #fff;
}
.posts > li + li{
    border-top: 1px solid #ddd;
}
.posts li .title span{
    position: absolute;
    left: 10px;
    top: 10px;
    color: #5FB878;
    font-size: 14px;
}
.posts li .title{
     margin-left: 40px;
     font-size: 20px;
     color: #222;
}
.posts .userAvator{
    position: absolute;
    left: 3px;
    top: 3px;
    width: 40px;
    height: 40px;
    border-radius: 5px;
}
.posts .content{
    border-top: 1px solid #f2f2f2;
    border-bottom: 1px solid #f2f2f2;
    margin: 10px 0 0 0 ;
    padding: 10px 0;
    margin-left: 40px;
}
.userMsg img{
    width: 40px;
    height: 40px;
    border-radius: 5px;
    margin-right: 10px;
    vertical-align: middle;
    display: inline-block;
}
.userMsg span{
    font-size: 18px;
    color:#333;
    position: relative;
    top: 2px;
}
.posts li img{
    max-width: 100%;
}
.spost .comment_pv{
    position: absolute;
    top: 10px;
}
.spost .edit {
    position: absolute;
    right: 20px;
    bottom: 5px;
}

.spost .edit p {
    display: inline-block;
    margin-left: 10px;
}

.comment_wrap {
    width: 60%;
    margin: 20px auto;
}

.submit {
    display: block;
    width: 100px;
    height: 40px;
    line-height: 40px;
    text-align: center;
    border-radius: 4px;
    background: #5FB878;
    cursor: pointer;
    color: #fff;
    float: left;
    margin-top: 20px ;
    border:1px solid #fff;
}
.submit:hover{
    background: #fff;
    color: #5FB878;
    border:1px solid #5FB878;
}
.comment_list{
    border: 1px solid #ddd;
    border-radius: 4px;
}
.cmt_lists:hover{
    background: #f2f2f2;
}
.cmt_lists + .cmt_lists{
    border-top: 1px solid #ddd;
}
.cmt_content {
    padding: 10px;
    position: relative;
    border-radius: 4px;
    word-break: break-all;
}
.cmt_detail{
    margin-left: 48px;
}
.cmt_content img{
    max-width: 100%;
}
/* .cmt_content:after {
    content: '#content';
    position: absolute;
    top: 5px;
    right: 5px;
    color: #aaa;
    font-size: 13px;
}
 */
.cmt_name {
    position: absolute;
    right: 8px;
    bottom: 5px;
    color: #333;
}

.cmt_name a {
    margin-left: 5px;
    color: #1E9FFF;
}
.cmt_time{
    position: absolute;
    font-size: 12px;
    right: 5px;
    top: 5px;
    color: #aaa
}
.form {
    margin: 0 auto;
    width: 50%;
    margin-top: 20px;
}

textarea {
    width: 100%;
    height: 150px;
    padding:10px 0 0 10px;
    font-size: 20px;
    border-radius: 4px;   
    border: 1px solid #d7dde4;
    -webkit-appearance: none;
    resize: none;
}

textarea#spContent{
    width: 98%;
}

.tips {
    margin: 20px 0;
    color: #ec5051;
    text-align: center;
}

.container {
    width: 60%;
    margin: 0 auto;
}
.form img.preview {
    width:100px;
    height:100px;
    border-radius: 50%;
    display: none;
    margin-top:10px;
}
input {
    display: block;
    width: 100%;
    height: 35px;
    font-size: 18px;
    padding: 6px 7px;    
    border-radius: 4px;   
    border: 1px solid #d7dde4;
    -webkit-appearance: none;
}

input:focus,textarea:focus{
    outline: 0;
    box-shadow: 0 0 0 2px rgba(51,153,255,.2);
    border-color: #5cadff;
}

input:hover,input:active,textarea:hover,textarea:active{
    border-color: #5cadff;
}

.create label {
    display: block;
    margin: 10px 0;
}

.comment_wrap form {
    width: 100%;
    margin-bottom: 85px;
}

.delete_comment,
.delete_post {
    cursor: pointer;
}

.delete_comment:hover,
.delete_post:hover,
a:hover {
    color: #ec5051;
}
.disabled{
    user-select: none;
    cursor: not-allowed !important;
}
.error{
    color: #ec5051;
}
.success{
    color: #1E9FFF;
}
.container{
    width: 60%;
    margin:0 auto;
}
.message{
    position: fixed;
    top: -100%;
    left: 50%;
    transform: translateX(-50%);
    padding: 10px 20px;
    background: rgba(0, 0, 0, 0.7);
    color: #fff;
    border-bottom-left-radius: 15px;
    border-bottom-right-radius: 15px;
    z-index: 99999;
}
.markdown pre{
    display: block;
    overflow-x: auto;
    padding: 0.5em;
    background: #F0F0F0;
    border-radius: 3px;
    border: 1px solid #fff;
}
.markdown blockquote{
    padding: 0 1em;
    color: #6a737d;
    border-left: 0.25em solid #dfe2e5;
    margin: 10px 0;
}
.markdown ul li{
    list-style: circle;
    margin-top: 5px;
}

我们再把模板引擎的header和footer独立出来

/views/header.ejs
顺便引入index.css和jq

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>koa2-blog</title>
    <link rel="icon" href="http://www.wclimb.site/images/avatar.png">
    <link rel="stylesheet" href="/index.css">
    <script data-original="http://cdn.bootcss.com/jquery/3.2.1/jquery.min.js"></script>
    <script>
        function fade(txt){
            $('.message').text(txt)
            $('.message').animate({
                top:0
            })
            setTimeout(function(){
                $('.message').animate({
                    top: '-100%'
                })
            },1500)
        }
        $(function(){
            $('.signout').click(()=>{
                $.ajax({
                    url: "/signout",
                    type: "GET",
                    cache: false,
                    dataType: 'json',
                    success: function (msg) {
                        if (msg) {
                            fade('登出成功')
                            setTimeout(()=>{
                                window.location.href = "/posts"
                            },1500)    
                        }
                    },
                    error: function () {
                        alert('异常');
                    }
                })
            })
        })
    </script>
</head>
<body>
    <header>
        <div class="user_name">
            <% if(session.user){ %>
                 Hello,<%= session.user %>
            <% } %>
            <% if(!session.user){ %>
                欢迎注册登录^_^
            <% } %>
        </div>
        <div class="message">登录成功</div>
        <div class="user_right">
            <%  if(session.user){ %>
                <div class="has_user">
                    <a target="__blank" href="https://github.com/wclimb/Koa2-blog">GitHub</a>
                    <% if(type == 'all'){ %>
                        <a class="active" href="/posts">全部文章</a>
                    <% }else{ %>
                        <a href="/posts">全部文章</a>
                    <% }%>
                    <% if(type == 'my'){ %>
                        <a class="active" href="/posts?author=<%= session.user %>">我的文章</a>
                    <% }else{ %>
                        <a href="/posts?author=<%= session.user %>">我的文章</a>
                    <% }%>
                    <% if(type == 'create'){ %>
                        <a class="active" href="/create">发表文章</a>
                    <% }else{ %>
                        <a href="/create">发表文章</a>
                    <% }%>
                    
                    <span class="signout">登出</span>
                </div>
            <% } %>
            <% if(!session.user){ %>
                <div class="none_user has_user">
                    <a target="__blank" href="https://github.com/wclimb/Koa2-blog">GitHub</a>
                    <% if(type == 'all'){ %>
                        <a class="active" href="/posts">全部文章</a>
                    <% }else{ %>
                        <a href="/posts">全部文章</a>
                    <% }%>
                    <% if(type == 'signup'){ %>
                        <a class="active" href="/signup">注册</a>
                    <% }else{ %>
                        <a href="/signup">注册</a>
                    <% }%>
                    <% if(type == 'signin'){ %>
                        <a class="active" href="/signin">登录</a>
                    <% }else{ %>
                        <a href="/signin">登录</a>
                    <% }%>
                </div>
            <% } %>
        </div>
    </header>

首先我们看到用到了session.user,这个值从哪来呢?请看下面的代码

// 注册页面
router.get('/signup', async(ctx, next) => {
    await checkNotLogin(ctx)
    await ctx.render('signup', {
        session: ctx.session,
    })
})

我们可以看到我们向模板传了一个session值,session:ctx.session,这个值存取的就是用户的信息,包括用户名、登录之后的id等,session一般是你关闭浏览器就过期了,等于下次打开浏览器的时候就得重新登录了,用if判断他存不存在,就可以知道用户是否需要登录,如果不存在用户,则只显示全部文章注册登录 ,如果session.user存在则有登出的按钮。

在上面我们会看到我用了另外一个if判断,判断type类型,这样做的目的是比如我们登录注册页面,注册页面的导航会高亮,其实就是添加了class:active;
之后我们每个ejs文件的头部会这样写<%- include("header",{type:'signup'}) %> 登录页面则是<%- include("header",{type:'signin'}) %>

/views/footer.ejs

    
</body>
</html>

修改views/signup.ejs

<%- include("header",{type:'signup'}) %>
    <div class="container">
        <form class="form create" method="post">
            <div>
                <label>用户名:</label> 
                <input placeholder="请输入用户名" type="text" name="name">
            </div>
            <div>
                <label>密码:</label> 
                <input placeholder="请输入密码" class="password" type="password" name="password">
            </div>
            <div>
                <label>重复密码:</label> 
                <input placeholder="请确认密码" class="repeatpass" type="password" name="repeatpass">
            </div>
            <div>
                <label>上传头像:</label>
                <input type="file" name="avator" id="avator">
                <input type="hidden" id="avatorVal">
                <img class="preview" alt="预览头像">
            </div>
            <div class="submit">注册</div>
        </form>
    </div>
    <script>
        $(window).keyup(function (e) {
            //console.log(e.keyCode)
            if (e.keyCode == 13) {
                $('.submit').click()
            }
        })
        $('#avator').change(function(){
            if (this.files.length != 0) {
                var file = this.files[0],
                    reader = new FileReader();
                if (!reader) {
                    this.value = '';
                    return;
                };
                console.log(file.size)
                if (file.size >= 1024 * 1024 / 2) {
                    fade("请上传小于512kb的图片!")
                    return 
                }
                reader.onload = function (e) {
                    this.value = '';
                    $('form .preview').attr('src', e.target.result)
                    $('form .preview').fadeIn()
                    $('#avatorVal').val(e.target.result)
                };
                reader.readAsDataURL(file);
            };
        })
        $('.submit').click(()=>{
            // console.log($('.form').serialize())
            if ($('input[name=name]').val().trim() == '') {
                fade('请输入用户名!')
            }else if($('input[name=name]').val().match(/[<'">]/g)){
                fade('请输入合法字符!')
            }else if($('#avatorVal').val() == ''){
                fade('请上传头像!')
            }else{
                $.ajax({
                    url: "/signup",
                    data: {
                        name: $('input[name=name]').val(),
                        password: $('input[name=password]').val(),
                        repeatpass: $('input[name=repeatpass]').val(),
                        avator: $('#avatorVal').val(),
                    },
                    type: "POST",
                    cache: false,
                    dataType: 'json',
                    success: function (msg) {
                       if (msg.data == 1) {                   
                           $('input').val('')
                           fade('用户名存在')
                       }
                       else if (msg.data == 2){
                            fade('请输入重复的密码')                           
                       }
                       else if(msg.data == 3){
                            fade('注册成功')
                            setTimeout(()=>{
                                window.location.href = "/signin"      
                            },1000)
                           }
                    },
                    error: function () {
                        alert('异常');
    
                    }
                })            
            }
        })        
    </script>
<% include footer %>

先看我们请求的url地址,是'/signup',为什么是这个呢?我们看下面这段代码(后面有完整的)

router.post('/signup', async(ctx, next) => {
    //console.log(ctx.request.body)
    let user = {
        name: ctx.request.body.name,
        pass: ctx.request.body.password,
        repeatpass: ctx.request.body.repeatpass,
        avator: ctx.request.body.avator
    }
    ....
}

我们的请求方式是post,地址是/signup所以走了这段代码,之后会获取我们输入的信息,通过ctx.request.body拿到

这里重点就在于ajax提交了,提交之后换回数据 1 2 3,然后分别做正确错误处理,把信息写在error和success中

修改routers/signup.js

const router = require('koa-router')();
const userModel = require('../lib/mysql.js');
const md5 = require('md5')
const checkNotLogin = require('../middlewares/check.js').checkNotLogin
const checkLogin = require('../middlewares/check.js').checkLogin
const moment = require('moment');
const fs = require('fs')
// 注册页面
router.get('/signup', async(ctx, next) => {
    await checkNotLogin(ctx)
    await ctx.render('signup', {
        session: ctx.session,
    })
})
// post 注册
router.post('/signup', async(ctx, next) => {
    //console.log(ctx.request.body)
    let user = {
        name: ctx.request.body.name,
        pass: ctx.request.body.password,
        repeatpass: ctx.request.body.repeatpass,
        avator: ctx.request.body.avator
    }
    await userModel.findDataByName(user.name)
        .then(async (result) => {
            console.log(result)
            if (result.length) {
                try {
                    throw Error('用户已经存在')
                } catch (error) {
                    //处理err
                    console.log(error)
                }
                // 用户存在
                ctx.body = {
                    data: 1
                };;
                
            } else if (user.pass !== user.repeatpass || user.pass === '') {
                ctx.body = {
                    data: 2
                };
            } else {
                // ctx.session.user=ctx.request.body.name   
                let base64Data = user.avator.replace(/^data:image\/\w+;base64,/, "");
                let dataBuffer = new Buffer(base64Data, 'base64');
                let getName = Number(Math.random().toString().substr(3)).toString(36) + Date.now()
                await fs.writeFile('./public/images/' + getName + '.png', dataBuffer, err => { 
                    if (err) throw err;
                    console.log('头像上传成功') 
                });            
                await userModel.insertData([user.name, md5(user.pass), getName, moment().format('YYYY-MM-DD HH:mm:ss')])
                    .then(res=>{
                        console.log('注册成功',res)
                        //注册成功
                        ctx.body = {
                            data: 3
                        };
                    })
            }
        })
})
module.exports = router
  • 我们使用md5实现密码加密,长度是32位的
  • 使用我们之前说的bodyParse来解析提交的数据,通过ctx.request.body得到
  • 我们引入了数据库的操作 findDataByName和insertData,因为之前我们在/lib/mysql.js中已经把他们写好,并暴露出来了。意思是先从数据库里面查找注册的用户名,如果找到了证明该用户名已经被注册过了,如果没有找到则使用insertData增加到数据库中
  • ctx.body 是我们通过ajax提交之后给页面返回的数据,比如提交ajax成功之后msg.data=1的时候就代表用户存在,msg.data出现在后面的signup.ejs模板ajax请求中
  • 上传头像之前要新建好文件夹,我们ajax发送的是base64的格式,然后使用fs.writeFile来生成图片

我们使用ajax来提交数据,方便来做成功错误的处理

模板引擎ejs

我们使用的是ejs,语法可以见ejs

之前我们写了这么一段代码

router.get('/signup',async (ctx,next)=>{
    await ctx.render('signup',{
        session:ctx.session,
    })
})

这里就用到了ejs所需的session 我们通过渲染signup.ejs模板,将值ctx.session赋值给session,之后我们就可以在signup.ejs中使用了
ejs的常用标签为:

  • <% code %>:运行 JavaScript 代码,不输出
  • <%= code %>:显示转义后的 HTML内容
  • <%- code %>:显示原始 HTML 内容

<%= code %><%- code %>的区别在于,<%= code %>不管你写什么都会原样输出,比如code为 <strong>hello</strong>的时候 <%= code %> 会显示<strong>hello</strong>
<%- code %>则显示加粗的hello

实现登录页面

img

修改 /routers/signin.js

const router = require('koa-router')();
const userModel = require('../lib/mysql.js')
const md5 = require('md5')
const checkNotLogin = require('../middlewares/check.js').checkNotLogin
const checkLogin = require('../middlewares/check.js').checkLogin

router.get('/signin', async(ctx, next) => {
    await checkNotLogin(ctx)
    await ctx.render('signin', {
        session: ctx.session,
    })
})
module.exports=router

修改 /views/signin.ejs

<%- include("header",{type:'signin'}) %>
    <div class="container">
        <form class="form create" method="post ">
            <div>
                <label>用户名:</label> 
                <input placeholder="用户名" type="text" name="name">
            </div>
            <div>
                <label>密码:</label> 
                <input placeholder="密码" type="password" name="password">
            </div>
            <div class="submit">登录</div>
        </form>        
    </div>
<% include footer %>

修改 index.js 文件 把下面这段代码注释去掉,之前注释是因为我们没有写signin的路由,以免报错,后面还有文章页和登出页的路由,大家记住一下

app.use(require('./routers/signin.js').routes())

现在注册一下来看看效果吧

$ supervisor --harmony index

img

我们怎么查看我们注册好的账号和密码呢?打开mysql控制台

$ select * from users;

这样刚刚我们注册的用户信息都出现了
img

如果你觉得这篇文章帮助到了你,那就赏脸给个star吧,https://github.com/wclimb/Koa...

登录页面

修改signin
routers/signin.js

const router = require('koa-router')();
const userModel = require('../lib/mysql.js')
const md5 = require('md5')
const checkNotLogin = require('../middlewares/check.js').checkNotLogin
const checkLogin = require('../middlewares/check.js').checkLogin

router.get('/signin', async(ctx, next) => {
    await checkNotLogin(ctx)
    await ctx.render('signin', {
        session: ctx.session,
    })
})

router.post('/signin', async(ctx, next) => {
    console.log(ctx.request.body)
    let name = ctx.request.body.name;
    let pass = ctx.request.body.password;

    await userModel.findDataByName(name)
        .then(result => {
            let res = result
            if (name === res[0]['name'] && md5(pass) === res[0]['pass']) {
                ctx.body = true
                ctx.session.user = res[0]['name']
                ctx.session.id = res[0]['id']
                console.log('ctx.session.id', ctx.session.id)
                console.log('session', ctx.session)
                console.log('登录成功')
            }else{
                ctx.body = false
                console.log('用户名或密码错误!')
            }
        }).catch(err => {
            console.log(err)
        })

})

module.exports = router

我们进行登录操作,判断登录的用户名和密码是否有误,使用md5加密
我们可以看到登录成功返回的结果是result 结果是这样的一个json数组:id:4 name:rrr pass:...
[ RowDataPacket { id: 4, name: 'rrr', pass: '44f437ced647ec3f40fa0841041871cd' } ]

修改views/signin.ejs
signin.ejs

<%- include("header",{type:'signin'}) %>
    <div class="container">
        <form class="form create" method="post ">
            <div>
                <label>用户名:</label> 
                <input placeholder="用户名" type="text" name="name">
            </div>
            <div>
                <label>密码:</label> 
                <input placeholder="密码" type="password" name="password">
            </div>
            <div class="submit">登录</div>
        </form>        
    </div>
    <script>
        $(window).keyup(function(e){
            //console.log(e.keyCode)
            if (e.keyCode == 13) {
                $('.submit').click()
            }
        })
        $('.submit').click(()=>{
            if ($('input[name=name]').val().trim() == '' || $('input[name=password]').val().trim() == '' ) {
                fade('请输入用户名或密码')
            }else{
                console.log($('.form').serialize())
                $.ajax({
                    url: "/signin",
                    data: $('.form').serialize(),
                    type: "POST",
                    cache: false,
                    dataType: 'json',
                    success: function (msg) {
                        if (!msg) {
                            $('input').val('')
                            fade('用户名或密码错误')
                        } else{
                            fade('登录成功')
                            setTimeout(()=>{
                                window.location.href = "/posts"
                            },1500)                       
                        }
                    },
                    error: function () {
                        alert('异常');
                    }
                })            
            }
        })        
    </script>
<% include footer %>

我们增加了ajax请求,在routers/signin.js里,我们设置如果登录失败就返回false,登录成功返回true

ctx.body = false
ctx.body = true

那我们登录成功后要做跳转,可以看到window.location.href="/posts"跳转到posts页面

全部文章

img
修改routers/posts.js

posts.js

const router = require('koa-router')();
const userModel = require('../lib/mysql.js')
const moment = require('moment')
const checkNotLogin = require('../middlewares/check.js').checkNotLogin
const checkLogin = require('../middlewares/check.js').checkLogin;
const md = require('markdown-it')();  
// 重置到文章页
router.get('/', async(ctx, next) => {
    ctx.redirect('/posts')
})
// 文章页
router.get('/posts', async(ctx, next) => {
    let res,
        postsLength,
        name = decodeURIComponent(ctx.request.querystring.split('=')[1]);
    if (ctx.request.querystring) {
        console.log('ctx.request.querystring', name)
        await userModel.findDataByUser(name)
            .then(result => {
                postsLength = result.length
            })
        await userModel.findPostByUserPage(name,1)
            .then(result => {
                res = result
            })
        await ctx.render('selfPosts', {
            session: ctx.session,
            posts: res,
            postsPageLength:Math.ceil(postsLength / 10),
        })
    } else {
        await userModel.findPostByPage(1)
            .then(result => {
                //console.log(result)
                res = result
            })
        await userModel.findAllPost()
            .then(result=>{
                postsLength = result.length
            })    
        await ctx.render('posts', {
            session: ctx.session,
            posts: res,
            postsLength: postsLength,
            postsPageLength: Math.ceil(postsLength / 10),
            
        })
    }
})
// 首页分页,每次输出10条
router.post('/posts/page', async(ctx, next) => {
    let page = ctx.request.body.page;
    await userModel.findPostByPage(page)
            .then(result=>{
                //console.log(result)
                ctx.body = result   
            }).catch(()=>{
            ctx.body = 'error'
        })  
})
// 个人文章分页,每次输出10条
router.post('/posts/self/page', async(ctx, next) => {
    let data = ctx.request.body
    await userModel.findPostByUserPage(data.name,data.page)
            .then(result=>{
                //console.log(result)
                ctx.body = result   
            }).catch(()=>{
            ctx.body = 'error'
        })  
})
module.exports = router

修改 index.js

app.use(require('./routers/posts.js').routes())的注释去掉

修改 views/posts.ejs

<%- include("header",{type:'posts'}) %>

    posts

<% include footer %>

现在看看登录成功之后的页面吧

接下来我们实现登出页面

登出页面

修改 router/signout.js

signout.js

const router = require('koa-router')();

router.get('/signout', async(ctx, next) => {
    ctx.session = null;
    console.log('登出成功')
    ctx.body = true
})

module.exports = router

把session设置为null即可

修改 index.js

app.use(require('./routers/posts.js').routes())的注释去掉,现在把注释的路由全部取消注释就对了

然后我们看看 views/header.ejs

我们点击登出后的ajax 的提交,成功之后回到posts页面

发表文章

修改router/posts
在后面增加

// 发表文章页面
router.get('/create', async(ctx, next) => {
    await ctx.render('create', {
        session: ctx.session,
    })
})

// post 发表文章
router.post('/create', async(ctx, next) => {
    let title = ctx.request.body.title,
        content = ctx.request.body.content,
        id = ctx.session.id,
        name = ctx.session.user,
        time = moment().format('YYYY-MM-DD HH:mm:ss'),
        avator,
        // 现在使用markdown不需要单独转义
        newContent = content.replace(/[<">']/g, (target) => { 
            return {
                '<': '&lt;',
                '"': '&quot;',
                '>': '&gt;',
                "'": '&#39;'
            }[target]
        }),
        newTitle = title.replace(/[<">']/g, (target) => {
            return {
                '<': '&lt;',
                '"': '&quot;',
                '>': '&gt;',
                "'": '&#39;'
            }[target]
        });

    //console.log([name, newTitle, content, id, time])
    await userModel.findUserData(ctx.session.user)
        .then(res => {
            console.log(res[0]['avator'])
            avator = res[0]['avator']       
        })
    await userModel.insertPost([name, newTitle, md.render(content), content, id, time,avator])
            .then(() => {
                ctx.body = true
            }).catch(() => {
                ctx.body = false
            })

})

修改 views/create.ejs

create.ejs
img

<%- include("header",{type:'create'}) %>
<div class="container">
    <form style="width:100%" method="post" class="form create">
        <div>
            <label>标题:</label>
            <input placeholder="请输入标题" type="text" name="title">
        </div>
        <div>
            <label>内容:</label>
            <textarea placeholder="请输入内容" name="content" id="" cols="42" rows="10"></textarea>
        </div>
        <div class="submit">发表</div>
    </form>
</div>
<script>
    $('.submit').click(()=>{
        if ($('input[name=title]').val().trim() == '') {
            fade('请输入标题')
        }else if ($('textarea').val().trim() == '') {
            fade('请输入内容')
        }else{          
            $.ajax({
                url: "/create",
                data: $('.form').serialize(),
                type: "POST",
                cache: false,
                dataType: 'json',
                success: function (msg) {
                    if (msg) {
                        fade('发表成功')
                        setTimeout(()=>{
                            window.location.href="/posts"
                        },1000)
                    }else{
                        fade('发表失败')
                    }
                },
                error: function () {
                    alert('异常');
                }
            })            
        }   
    })
</script>
<% include footer %>

现在看看能不能发表吧

即使我们发表了文章,但是当前我们的posts的页面没有显示,因为还没有获取到数据

我们可以看我们之前写的代码 router.get('/posts', async(ctx, next) => {}路由

if (ctx.request.querystring) {
    ...
}else {
        await userModel.findPostByPage(1)
            .then(result => {
                //console.log(result)
                res = result
            })
        await userModel.findAllPost()
            .then(result=>{
                postsLength = result.length
            })    
        await ctx.render('posts', {
            session: ctx.session,
            posts: res,
            postsLength: postsLength,
            postsPageLength: Math.ceil(postsLength / 10),
            
        })
    }

if前面这部分我们先不用管,后面会说。只需要看else后面的代码我们通过userModel.findPostByPage(1)来获取第一页的数据,然后查找所有文章的数量,最后除以10拿到首页文章的页数,把数据postsPageLength的值传给模板posts.ejs。这样就可以渲染出来了

修改 views/posts.ejs

posts.ejs

<%- include("header",{type:'all'}) %>
    <div class="container">
        <ul class="posts">
            <% posts.forEach(function(res){ %>
                <li>
                    <div class="author">
                        <span title="<%= res.name %>"><a href="/posts?author=<%= res.name %> ">author: <%= res.name %></a></span>
                        <span>评论数:<%= res.comments %></span>
                        <span>浏览量:<%= res.pv %></span>
                    </div>
                    <div class="comment_pv">
                        <span><%= res.moment %></span>
                    </div>
                    <a href="/posts/<%= res.id %>">
                        <div class="title">
                            <img class="userAvator" data-original="images/<%= res.avator %>.png">
                            <%= res.title %>
                        </div>
                        <div class="content markdown">
                            <%- res.content %>
                        </div>
                    </a>
                </li>
            <% }) %>
        </ul>
        <div style="margin-top: 30px" class="pagination" id="page"></div>    
    </div>
    <script data-original="http://www.wclimb.site/pagination/pagination.js"></script>
    <script>
        pagination({
            selector: '#page',
            totalPage: <%= postsPageLength %>,
            currentPage: 1,
            prev: '上一页',
            next: '下一页',
            first: true,
            last: true,
            showTotalPage: true,
            count: 2//当前页前后显示的数量
        },function(val){
            // 当前页
            $.ajax({
                url: "posts/page",
                type: 'POST',
                data:{
                    page: val
                },
                cache: false,
                success: function (msg) {
                    console.log(msg)
                    if (msg != 'error') {
                        $('.posts').html(' ')
                        $.each(msg,function(i,val){
                            //console.log(val.content)
                            $('.posts').append(
                                '<li>'+
                                    '<div class=\"author\">'+
                                        '<span title=\"'+ val.name +'\"><a href=\"/posts?author='+ val.name +' \">author: '+ val.name +'</a></span>'+
                                        '<span>评论数:'+ val.comments +'</span>'+
                                        '<span>浏览量:'+ val.pv +'</span>'+
                                    '</div>'+
                                    '<div class=\"comment_pv\">'+
                                        '<span>'+ val.moment +'</span>'+
                                    '</div>'+
                                    '<a href=\"/posts/'+ val.id +'\">'+
                                        '<div class=\"title\">'+
                                            '<img class="userAvator" data-original="images/'+ val.avator +'.png">'+
                                             val.title +
                                        '</div>'+
                                        '<div class=\"content\">'+
                                             val.content +
                                        '</div>'+
                                    '</a>'+
                                '</li>'
                            )
                        })
                    }else{
                        alert('分页不存在')
                    } 
                }
            })
        })
    </script>
<% include footer %>

现在看看posts页面有没有文章出现了

我们看到是所第一页的文章数据,初始化的稍后我们是用服务端渲染的数据,使用了分页,每页显示10条数据,然后通过请求页数。
下面是服务端请求拿到的第一页的数据

await userModel.findPostByUserPage(name,1)
        .then(result => {
            res = result
        })

要拿到别的页面数据的话要向服务器发送post请求,如下

// 首页分页,每次输出10条
router.post('/posts/page', async(ctx, next) => {
    let page = ctx.request.body.page;
    await userModel.findPostByPage(page)
            .then(result=>{
                //console.log(result)
                ctx.body = result   
            }).catch(()=>{
            ctx.body = 'error'
        })  
})

单篇文章页面

img
但是我需要点击单篇文章的时候,显示一篇文章怎么办呢?

修改 routers/posts.js

在posts.js后面增加

// 单篇文章页
router.get('/posts/:postId', async(ctx, next) => {
    let comment_res,
        res,
        pageOne,
        res_pv; 
    await userModel.findDataById(ctx.params.postId)
        .then(result => {
            //console.log(result )
            res = result
            res_pv = parseInt(result[0]['pv'])
            res_pv += 1
           // console.log(res_pv)
        })
    await userModel.updatePostPv([res_pv, ctx.params.postId])
    await userModel.findCommentByPage(1,ctx.params.postId)
        .then(result => {
            pageOne = result
            //console.log('comment', comment_res)
        })
    await userModel.findCommentById(ctx.params.postId)
        .then(result => {
            comment_res = result
            //console.log('comment', comment_res)
        })
    await ctx.render('sPost', {
        session: ctx.session,
        posts: res[0],
        commentLenght: comment_res.length,
        commentPageLenght: Math.ceil(comment_res.length/10),
        pageOne:pageOne
    })

})

现在的设计是,我们点进去出现的url是 /posts/1 这类的 1代表该篇文章的id,我们在数据库建表的时候就处理了,让id为主键,然后递增

我们通过userModel.findDataById 文章的id来查找数据库
我们通过userModel.findCommentById 文章的id来查找文章的评论,因为单篇文章里面有评论的功能
最后通过sPost.ejs模板渲染单篇文章

修改 views/sPost.ejs

sPost.ejs

<%- include("header",{type:''}) %>
    <div class="container">
        <ul class="posts spost">
            <li>
                <div class="author">
                    <span title="<%= posts.name %>"><a href="/posts?author=<%= posts.name %> ">author: <%= posts.name %></a></span>
                    <span>评论数:<%= posts.comments %></span>
                    <span>浏览量:<%= posts.pv %></span>
                </div>
                <div class="comment_pv">
                    <span><%= posts.moment %></span>
                </div>
                <a href="/posts/<%= posts.id %>">
                    <div class="title">
                        <img class="userAvator" data-original="../images/<%= posts.avator %>.png">
                        <%= posts.title %>
                    </div>
                    <div class="content markdown">
                        <%- posts.content %>
                    </div>
                </a>
                <div class="edit">
                    <% if(session && session.user ===  posts.name  ){ %>
                    <p><a href="<%= posts['id'] %>/edit">编辑</a></p>
                    <p><a class="delete_post">删除</a></p>
                    <% } %>
                </div>
            </li>
        </ul>
    </div>
    <div class="comment_wrap">
        <% if(session.user){ %>
        <form class="form" method="post" action="/<%= posts.id %>">
            <textarea id="spContent" name="content" cols="82"></textarea>
            <div class="submit">发表留言</div>
        </form>
        <% } else{ %>
            <p class="tips">登录之后才可以评论哟</p>
        <% } %>
        <% if (commentPageLenght > 0) { %>
        <div class="comment_list markdown">
            <% pageOne.forEach(function(res){ %>
                <div class="cmt_lists">
                    <div class="cmt_content">
                        <div class="userMsg">
                            <img data-original="../images/<%= res['avator'] %>.png" alt=""><span><%= res['name'] %></span>
                        </div>
                        <div class="cmt_detail">
                            <%- res['content'] %>
                        </div>
                        <span class="cmt_time"><%= res['moment'] %></span>
                        <span class="cmt_name">
                            <% if(session && session.user ===  res['name']){ %>
                                <a class="delete_comment" href="javascript:delete_comment(<%= res['id'] %>);"> 删除</a>
                            <% } %>
                        </span>
                    </div>
                </div>
            <% }) %>
        </div>    
        <% } else{ %>
            <p class="tips">还没有评论,赶快去评论吧!</p>
        <% } %>
        <div style="margin-top: 30px" class="pagination" id="page"></div>    
    </div>
    <script data-original="http://www.wclimb.site/pagination/pagination.js"></script>
    <script>
        var userName = "<%- session.user %>"
        pagination({
            selector: '#page',
            totalPage: <%= commentPageLenght %>,
            currentPage: 1,
            prev: '上一页',
            next: '下一页',
            first: true,
            last: true,
            showTotalPage:true,
            count: 2//当前页前面显示的数量
        },function(val){
            // 当前页
            var _comment = ''
            $.ajax({
                url: "<%= posts.id %>/commentPage",
                type: 'POST',
                data:{
                    page: val
                },
                cache: false,
                success: function (msg) {
                    //console.log(msg)
                    _comment = ''
                    if (msg != 'error') {
                        $('.comment_list').html(' ')
                        $.each(msg,function(i,val){
                            //console.log(val.content)
                            _comment += '<div class=\"cmt_lists\"><div class=\"cmt_content\"><div class=\"userMsg\"><img src = \"../images/'+ val.avator +'.png\" ><span>'+ val.name +'</span></div ><div class="cmt_detail">'+ val.content + '</div><span class=\"cmt_time\">'+ val.moment +'</span><span class=\"cmt_name\">';
                                if (val.name == userName) {
                                    _comment += '<a class=\"delete_comment\" href=\"javascript:delete_comment('+ val.id +');\"> 删除</a>'
                                }
                            _comment += '</span></div></div>'
                        })
                        $('.comment_list').append(_comment)
                    }else{
                        alert('分页不存在')
                    } 
                }
            })
        
        })
        
        // 删除文章
        $('.delete_post').click(() => {
            $.ajax({
                url: "<%= posts.id %>/remove",
                type: 'POST',
                cache: false,
                success: function (msg) {
                    if (msg.data == 1) {
                        fade('删除文章成功')
                        setTimeout(() => {
                            window.location.href = "/posts"
                        }, 1000)
                    } else if (msg.data == 2) {
                        fade('删除文章失败');
                        setTimeout(() => {
                            window.location.reload()
                        }, 1000)
                    }
                }
            })
        })
        // 评论
        var isAllow = true
        $('.submit').click(function(){
            if (!isAllow) return
            isAllow = false
            if ($('textarea').val().trim() == '') {
                fade('请输入评论!')
            }else{
                $.ajax({
                    url: '/' + location.pathname.split('/')[2],
                    data:$('.form').serialize(),
                    type: "POST",
                    cache: false,
                    dataType: 'json',
                    success: function (msg) {
                        if (msg) {
                            fade('发表留言成功')                            
                            setTimeout(()=>{
                                isAllow = true
                                window.location.reload()
                            },1500)      
                        }
                    },
                    error: function () {
                        alert('异常');
                    }
                })
            }
        })
        // 删除评论
        function delete_comment(id) {
            $.ajax({
                url: "<%= posts.id %>/comment/" + id + "/remove",
                type: 'POST',
                cache: false,
                success: function (msg) {
                    if (msg.data == 1) {
                        fade('删除留言成功')
                        setTimeout(() => {
                            window.location.reload()
                        }, 1000)
                    } else if (msg.data == 2) {
                        fade('删除留言失败');
                        setTimeout(() => {
                            window.location.reload()
                        }, 1500)
                    }
                },
                error: function () {
                    alert('异常')
                }
            })
        }
    </script>
<% include footer %>

现在点击单篇文章试试,进入单篇文章页面,但是编辑、删除、评论都还没有做,点击无效,我们先不做,先实现每个用户自己发表的文章列表,我们之前在 get '/posts' 里面说先忽略if (ctx.request.querystring) {}里面的代码,这里是做了一个处理,假如用户点击了某个用户,该用户发表了几篇文章,我们需要只显示该用户发表的文章,那么进入的url应该是 /posts?author=xxx ,这个处理在posts.ejs 就已经加上了,就在文章的左下角,作者:xxx就是一个链接。我们通过判断用户来查找文章,继而有了ctx.request.querystring 获取到的是:author=xxx

注:这里我们处理了,通过判断 session.user === res['name'] 如果不是该用户发的文章,不能编辑和删除,评论也是。这里面也可以注意一下包裹的<a href=""></a>标签

个人已发表文章列表里面

还记得之前在 get '/post' 里面的代码吗?
下面的代码就是之前说先不处理的代码片段,不过这个不用再次添加,之前已经添加好了,这段代码处理个人发布的文章列表,我们是通过selfPosts.ejs模板来渲染的,样式和全部文章列表一样,但是牵扯到分页的问题,
分页请求的是不同的接口地址

if (ctx.request.querystring) {
        console.log('ctx.request.querystring', name)
        await userModel.findDataByUser(name)
            .then(result => {
                postsLength = result.length
            })
        await userModel.findPostByUserPage(name,1)
            .then(result => {
                res = result
            })
        await ctx.render('selfPosts', {
            session: ctx.session,
            posts: res,
            postsPageLength:Math.ceil(postsLength / 10),
        })
    }

修改 selfPost.ejs

<%- include("header",{type:'my'}) %>
    <div class="container">
        <ul class="posts">
            <% posts.forEach(function(res){ %>
                <li>
                    <div class="author">
                        <span title="<%= res.name %>"><a href="/posts?author=<%= res.name %> ">author: <%= res.name %></a></span>
                        <span>评论数:<%= res.comments %></span>
                        <span>浏览量:<%= res.pv %></span>
                    </div>
                    <div class="comment_pv">
                        <span><%= res.moment %></span>
                    </div>
                    <a href="/posts/<%= res.id %>">
                        <div class="title">
                            <img class="userAvator" data-original="images/<%= res.avator %>.png">
                            <%= res.title %>
                        </div>
                        <div class="content markdown">
                            <%- res.content %>
                        </div>
                    </a>
                </li>
            <% }) %>
        </ul>
        <div style="margin-top: 30px" class="pagination" id="page"></div>    
    </div>
    <script data-original="http://www.wclimb.site/pagination/pagination.js"></script>
    <script>
        pagination({
            selector: '#page',
            totalPage: <%= postsPageLength %>,
            currentPage: 1,
            prev: '上一页',
            next: '下一页',
            first: true,
            last: true,
            showTotalPage: true,
            count: 2//当前页前后显示的数量
        },function(val){
            // 当前页
            $.ajax({
                url: "posts/self/page",
                type: 'POST',
                data:{
                    page: val,
                    name: location.search.slice(1).split('=')[1]
                },
                cache: false,
                success: function (msg) {
                    //console.log(msg)
                    if (msg != 'error') {
                        $('.posts').html(' ')
                        $.each(msg,function(i,val){
                            //console.log(val.content)
                            $('.posts').append(
                                '<li>'+
                                    '<div class=\"author\">'+
                                        '<span title=\"'+ val.name +'\"><a href=\"/posts?author='+ val.name +' \">author: '+ val.name +'</a></span>'+
                                        '<span>评论数:'+ val.comments +'</span>'+
                                        '<span>浏览量:'+ val.pv +'</span>'+
                                    '</div>'+
                                    '<div class=\"comment_pv\">'+
                                        '<span>'+ val.moment +'</span>'+
                                    '</div>'+
                                    '<a href=\"/posts/'+ val.id +'\">'+
                                        '<div class=\"title\">'+
                                            '<img class="userAvator" data-original="images/' + val.avator + '.png">' +
                                             val.title +
                                        '</div>'+
                                        '<div class=\"content\">'+
                                             val.content +
                                        '</div>'+
                                    '</a>'+
                                '</li>'
                            )
                        })
                    }else{
                        alert('分页不存在')
                    } 
                }
            })
        
        })
    </script>
<% include footer %>

编辑文章、删除文章、评论、删除评论

评论

修改routers/posts.js

在post.js 后面增加

// 发表评论
router.post('/:postId', async(ctx, next) => {
    let name = ctx.session.user,
        content = ctx.request.body.content,
        postId = ctx.params.postId,
        res_comments,
        time = moment().format('YYYY-MM-DD HH:mm:ss'),
        avator;
    await userModel.findUserData(ctx.session.user)
        .then(res => {
            console.log(res[0]['avator'])
            avator = res[0]['avator']
        })   
    await userModel.insertComment([name, md.render(content),time, postId,avator])
    await userModel.findDataById(postId)
        .then(result => {
            res_comments = parseInt(result[0]['comments'])
            res_comments += 1
        })
    await userModel.updatePostComment([res_comments, postId])
        .then(() => {
            ctx.body = true
        }).catch(() => {
            ctx.body = false
        })
})
// 评论分页
router.post('/posts/:postId/commentPage', async function(ctx){
    let postId = ctx.params.postId,
        page = ctx.request.body.page;
    await userModel.findCommentByPage(page,postId)
        .then(res=>{
            ctx.body = res
        }).catch(()=>{
            ctx.body = 'error'
        })  
})

现在试试发表评论的功能吧,之所以这样简单,因为我们之前就在sPost.ejs做了好几个ajax的处理,删除文章和评论也是如此
评论我们也做了分页,所以后面会有一个评论的分页的接口,我们已经在sPost.ejs里面写好了ajax请求

删除评论

修改routers/posts.js

继续在post.js 后面增加

// 删除评论
router.post('/posts/:postId/comment/:commentId/remove', async(ctx, next) => {
    let postId = ctx.params.postId,
        commentId = ctx.params.commentId,
        res_comments;
    await userModel.findDataById(postId)
        .then(result => {
            res_comments = parseInt(result[0]['comments'])
            //console.log('res', res_comments)
            res_comments -= 1
            //console.log(res_comments)
        })
    await userModel.updatePostComment([res_comments, postId])
    await userModel.deleteComment(commentId)
        .then(() => {
            ctx.body = {
                data: 1
            }
        }).catch(() => {
            ctx.body = {
                data: 2
            }

        })
})

现在试试删除评论的功能吧

删除文章

只有自己发表的文字删除的文字才会显示出来,才能被删除,

修改routers/posts.js

继续在post.js 后面增加

// 删除单篇文章
router.post('/posts/:postId/remove', async(ctx, next) => {
    let postId = ctx.params.postId
    await userModel.deleteAllPostComment(postId)
    await userModel.deletePost(postId)
        .then(() => {
            ctx.body = {
                data: 1
            }
        }).catch(() => {
            ctx.body = {
                data: 2
            }
        })
})

现在试试删除文章的功能吧

编辑文章

修改routers/posts.js

继续在post.js 后面增加

// 编辑单篇文章页面
router.get('/posts/:postId/edit', async(ctx, next) => {
    let name = ctx.session.user,
        postId = ctx.params.postId,
        res;
    await userModel.findDataById(postId)
        .then(result => {
            res = result[0]
            //console.log('修改文章', res)
        })
    await ctx.render('edit', {
        session: ctx.session,
        postsContent: res.md,
        postsTitle: res.title
    })

})

// post 编辑单篇文章
router.post('/posts/:postId/edit', async(ctx, next) => {
    let title = ctx.request.body.title,
        content = ctx.request.body.content,
        id = ctx.session.id,
        postId = ctx.params.postId,
         // 现在使用markdown不需要单独转义
        newTitle = title.replace(/[<">']/g, (target) => {
            return {
                '<': '&lt;',
                '"': '&quot;',
                '>': '&gt;',
                "'": '&#39;'
            }[target]
        }),
        newContent = content.replace(/[<">']/g, (target) => {
            return {
                '<': '&lt;',
                '"': '&quot;',
                '>': '&gt;',
                "'": '&#39;'
            }[target]
        });
    await userModel.updatePost([newTitle, md.render(content), content, postId])
        .then(() => {
            ctx.body = true
        }).catch(() => {
            ctx.body = false
        })
})

修改views/edit.js

<%- include("header",{type:''}) %>
<div class="container">
    <form style="width:100%" class="form create" method="post">
        <div>
            <label>标题:</label>
            <input placeholder="标题" type="text" name="title" value="<%- postsTitle %>">
        </div>
        <div>
            <label>内容:</label>
            <textarea name="content" id="" cols="42" rows="10"><%= postsContent %></textarea>
        </div>
        <div class="submit">修改</div>
    </form>
</div>
<script>
    $('.submit').click(()=>{
        $.ajax({
            url: '',
            data: $('.form').serialize(),
            type: "POST",
            cache: false,
            dataType: 'json',
            success: function (msg) {
               if (msg) {
                       fade('修改成功')
                       setTimeout(()=>{
                           window.location.href="/posts"
                       },1000)
               }
            },
            error: function () {
                alert('异常');
            }
        })        
    })
</script>
<% include footer %>

现在试试编辑文字然后修改提交吧

结语

至此一个简单的blog就已经制作好了,其他扩展功能相信你已经会了吧!如果出现问题,还望积极提问哈,我会尽快处理的

所有的代码都在 https://github.com/wclimb/Koa... 里面,如果觉得不错就star一下吧。有问题可以提问哟
下一篇可能是 Node + express + mongoose 或 zepto源码系列
感谢您的阅读^_^

查看原文

刘宁Leo 关注了专栏 · 2018-08-16

前端每日实战

?该专栏由《CSS3 艺术》一书的作者亲自维护,已累计分享 170+ 个前端项目从灵感闪现到代码实现的完整过程。?

关注 5289

认证与成就

  • 获得 603 次点赞
  • 获得 4 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 4 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2018-02-09
个人主页被 4.6k 人浏览