实现的效果如下:
界面可能不是太好看?,考虑到容器的高度会被拉长,因此没有用图片做背景。
预览
涉及的知识点
-
sass
(css 预编译器) -
webpack
(自动化构建工具,实现LESS,CSS,JS编译和压缩代码) -
express
(基于 Node.js 平台的 web 开发框架) html+css
-
Node.js
(基于 Chrome V8 引擎的 JavaScript 运行环境) -
jQuery
(一个快速、简洁的JavaScript框架) -
sequelize
(Node的ORM框架Sequelize操作数据库) -
passport
(实现第三方登录)
实现功能
- github第三方登录
- 添加笔记(登录成功后)
- 删除笔记
- 修改笔记
- 使用 markdown(类似 typroa)
- 笔记拖拽
准备工作
- 必要条件:已经安装好了node环境,还没安装的可以去node中文官网下载
-
小提示:如果用 npm 下载感觉慢的话,可以下载一个切换镜像源的工具
nrm
,在终端输入:
npm i nrm -g
然后如下操作:
开始!!
1.新建一个文件夹,名字自己起,打开终端,切换到自己新建文件夹,如:
cd (文件夹名称)
2.生成 package.json
npm init -y
3.安装 express
npm i express --save
4.安装 express生成器:
npm install express-generator --save
5.生成 ejs 模板(类似 jsp 的写法)
express -f -e
npm i
其中public
用来存放编译后的js文件以及编译好的css文件等,routes
用来存放处理 ajax 的请求文件,views就是存放视图文件
然后新建 database 和 src:
其中 src/js 里面 app 代表不同页面的入口文件,lib 就是一些常用的库,mod 就是你写的一些模块,database 用来存放数据库数据的
6.输入:
npm start
如果有出现下面的错误:
出现这个错误是因为你没有下载模块,只需在终端输入:
npm i (模块名) --save
就可以了
7.打开浏览器,输入localhost:3000
出现下面这样就说明成功了:
8.接下来安装webpack和相关依赖
npm i webpack --save-dev
npm i --save css-loader style-loader express-session express-flash node-sass passport sass sass-loader sequelize sqlite3 extract-text-webpack-plugin onchange
9.在 src 里建一个 webpack.config.js,配置如下:
var webpack = require('webpack');
var path = require('path');
var ExtractTextPlugin = require('extract-text-webpack-plugin')
var autoprefixer = require('autoprefixer');
module.exports = {
entry: path.join(__dirname, "js/app/index"),
output: {
path: path.join(__dirname, "../public"),
filename: "js/index.js"
},
module: {
rules: [{
test: /(\.scss)$/,
use: ExtractTextPlugin.extract({
fallback: "style-loader",
use: ["css-loader", "sass-loader"]
}) //把 css 抽离出来生成一个文件
}]
},
resolve: {
alias: {
jquery: path.join(__dirname, "js/lib/jquery-2.0.3.min.js"),
mod: path.join(__dirname, "js/mod"),
sass: path.join(__dirname, "sass")
}
},
plugins: [
new webpack.ProvidePlugin({
$: "jquery"
}),
new ExtractTextPlugin("css/index.css"),
new webpack.LoaderOptionsPlugin({
options: {
css: [
autoprefixer(),
]
}
}),
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
}
})
]
}
说明
- entry:入口文件,也就是 src/js/app里面的index.js,其中__dirname是获得当前文件所在目录的完整目录名
- output:输出编译后的文件 index.js,输出到 public/js 里面
- module:配置Loaders,通过使用不同的loader,webpack有能力调用外部的脚本或工具,实现对不同格式的文件的处理,比如说分析转换scss为css,或者把下一代的JS文件
- resolve.alias:设置模块别名,便于我们更方便引用,比如说我在 js里面的文件需要 jquery,在里面的文件直接写 require("jquery") 就行了
- 如果所有文件都需要 jquery,那么直接在 plugins里面写成这样:
就不需要 require 了
这个是压缩文件的
10.在 package.json 中,增加如下两条:
写成这样,你在终端就可以写成npm run webpack
来编译文件,npm run watch
来监控 src 里面的 js 和 scss 的变化,只要一修改,进行编译,提高了效率
11.测试
你可以试试在 js 里面的 index.js写点东西,然后 npm run webpack,如果终端显示是这样:
就证明成功了
项目思路
逻辑比较简单
- 首先用户必须登录才能添加笔记,当用户失焦的时候,将数据插入数据库,并且重新布局(瀑布流)
- 用户不能更改其他用户的笔记,除了管理员?
- 用户更新笔记之后,数据库的数据重新更新,重新布局
- 用户可以删除笔记,数据从数据库中删除,重新布局
- 用户可以拖拽笔记,但不将位置存入数据库
实现
html,css就不讲了,可以看看我的源码,主要讲 js。
1.瀑布流的实现
思路:(前提是必须绝对定位)
- 获取元素的宽度
- 通过窗口的宽度除以元素的宽度来获取列数
- 初始化一个数组来获取每列的高度,初始化每列的高度为0
遍历元素,获取最小的的列数的高度和索引,对当前元素进行定位,列数高度加等于当前元素的高度
知道思路后,代码很快就写出来了:
var WaterFall = (function () {
var $ct, $items;
function render($c) {
$ct = $c;
$items = $ct.children();
var nodeWidth = $items.outerWidth(true),
windowHeight = $(window).height(),
colNum = parseInt($(window).width() / nodeWidth), //获取列数
colSumHeight = []; //获取每列的高度
//对每列的高度进行初始化
for (var i = 0; i < colNum; i++) {
colSumHeight[i] = 0;
}
$items.each(function () {
var $current = $(this);
var index = 0,
minSumHeight = colSumHeight[0];
//获取最小的的列数的高度和索引
for (var i = 0; i < colSumHeight.length; i++) {
if (minSumHeight > colSumHeight[i]) {
index = i;
minSumHeight = colSumHeight[i];
}
}
//改变窗口高度
if (windowHeight < minSumHeight) {
$("body").height(minSumHeight);
} else {
$("body").height(windowHeight - 72);
}
//对当前元素进行定位
$current.animate({
left: nodeWidth * index,
top: minSumHeight
}, 5);
colSumHeight[index] += $current.outerHeight(true);
});
}
//当窗口发生变化时,重新渲染
$(window).on('resize', function () {
render($ct);
});
return {
init: render
}
})();
2.笔记的拖拽
我们先看个图
因此代码如下:
//设置笔记的移动
$noteHead.on('mousedown', function (e) {
var evtX = e.pageX - $note.offset().left, //evtX 计算事件的触发点在 dialog内部到 dialog 的左边缘的距离
evtY = e.pageY - $note.offset().top;
$note.addClass('draggable').data('evtPos', {
x: evtX,
y: evtY
}); //把事件到 dialog 边缘的距离保存下来
}).on('mouseup', function () {
$note.removeClass('draggable').removeData('pos');
});
$('body').on('mousemove', function (e) {
$('.draggable').length && $('.draggable').offset({
top: e.pageY - $('.draggable').data('evtPos').y, // 当用户鼠标移动时,根据鼠标的位置和前面保存的距离,计算 dialog 的绝对位置
left: e.pageX - $('.draggable').data('evtPos').x
});
});
},
3.提示模块
这个比较容易:
/*
提示模块
参数:状态(1表示成功,0表示失败),消息,出现时间(不写默认是1s)
*/
function toast(status, msg, time) {
this.status = status;
this.msg = msg;
this.time = time || 1000;
this.createToast();
this.showToast();
}
toast.prototype = {
createToast: function () {
if (this.status === 1) {
var html = '<div class="toast">![](../../imgs/1.png)</img><span class="toast_word">' + this.msg + '</span></div>';
this.$toast = $(html);
$('body').append(this.$toast);
} else {
var html = '<div class="toast">![](../../imgs/0.png)</img><span class="toast_word">' + this.msg + '</span></div>';
this.$toast = $(html);
$('body').append(this.$toast);
}
},
showToast: function () {
var _this = this;
this.$toast.fadeIn(300, function () {
setTimeout(function () {
_this.$toast.fadeOut(300, function () {
_this.$toast.remove();
});
}, _this.time);
})
}
}
function Toast(status, msg, time) {
return new toast(status, msg, time);
}
4.笔记模块
思路:
- 初始化(如 id,username 等等)
- 创建节点
- 设置颜色
绑定事件
function Note(opts) {
this.initOpts(opts);
this.createNode();
this.setColor();
this.bind();
}
Note.prototype = {
colors: [
['#ea9b35', '#efb04e'], // headColor, containerColor
['#dd598b', '#e672a2'],
['#c24226', '#d15a39'],
['#c1c341', '#d0d25c'],
['#3f78c3', '#5591d2']
],
defaultOpts: {
id: '', //Note的 id
$ct: $('#content').length > 0 ? $('#content') : $('body'), //默认存放 Note 的容器
context: '请输入内容', //Note 的内容
createTime: new Date().toLocaleDateString().replace(/\//g, '-').match(/^\d{4}-\d{1,2}-\d{1,2}/),
username: 'admin'
},
initOpts: function (opts) {
this.opts = $.extend({}, this.defaultOpts, opts || {});
if (this.opts.id) {
this.id = this.opts.id;
}
this.createTime = this.opts.createTime ? this.opts.createTime : new Date().toLocaleDateString().replace(/\//g, '-').match(/^\d{4}-\d{1,2}-\d{1,2}/);
this.username = this.opts.username ? this.opts.username : 'admin'
},
createNode: function () {
var tpl = '<div class="note">' +
'<div class="note-head"><span class="delete">×</span></div>' +
'<div class="note-ct" contenteditable="true"></div>' +
'<div class="note-info"><div class="note-name">' + this.username + '</div><div class="note-time">' + this.createTime + '</div>' +
'</div>';
this.$note = $(tpl);
this.$note.find('.note-ct').html(this.opts.context);
this.opts.$ct.append(this.$note);
//if (!this.id) this.$note.css('bottom', '10px'); //新增放到右边
Event.fire('waterfall');
},
setColor: function () {
var color = this.colors[Math.floor(Math.random() * 5)];
this.$note.find(".note-head").css('background-color', color[0]);
this.$note.find('.note-ct').css('background-color', color[1]);
this.$note.find('.note-info').css('background-color', color[1]);
},
setLayout: function () {
var self = this;
if (self.clock) {
clearTimeout(self.clock);
}
self.clock = setTimeout(function () {
Event.fire('waterfall');
}, 100);
},
bind: function () {
var _this = this, //记录下坑,之前末尾是分号不是逗号后面都变成了全局变量结果造成了最后一个才能修改?
$note = this.$note,
$noteHead = $note.find('.note-head'),
$noteCt = $note.find('.note-ct'),
$close = $note.find('.delete');
$close.on('click', function () {
_this.delete();
});
$noteCt.on('focus', function () {
if ($noteCt.html() === '请输入内容') $noteCt.html('');
$noteCt.data('before', $noteCt.html());
}).on('blur paste', function () {
if ($noteCt.data('before') != $noteCt.html()) {
$noteCt.data('before', $noteCt.html());
_this.setLayout();
if (_this.id) { //判断是否有这个id,如果有就更新,如果没有就添加
_this.edit($noteCt.html())
} else {
_this.add($noteCt.html())
}
}
});
//设置笔记的移动
$noteHead.on('mousedown', function (e) {
var evtX = e.pageX - $note.offset().left, //evtX 计算事件的触发点在 dialog内部到 dialog 的左边缘的距离
evtY = e.pageY - $note.offset().top;
$note.addClass('draggable').data('evtPos', {
x: evtX,
y: evtY
}); //把事件到 dialog 边缘的距离保存下来
}).on('mouseup', function () {
$note.removeClass('draggable').removeData('pos');
});
$('body').on('mousemove', function (e) {
$('.draggable').length && $('.draggable').offset({
top: e.pageY - $('.draggable').data('evtPos').y, // 当用户鼠标移动时,根据鼠标的位置和前面保存的距离,计算 dialog 的绝对位置
left: e.pageX - $('.draggable').data('evtPos').x
});
});
},
/* 添加笔记到数据库 */
add: function (msg) {
var _this = this;
$.post('/api/notes/add', {
note: msg
}).done(function (res) {
if (res.status === 1) {
_this.id = res.id;
Toast(1, '添加成功!');
} else {
_this.$note.remove();
Event.fire('waterfall');
Toast(0, res.errorMsg);
}
})
},
/* 编辑笔记数据库 */
edit: function (msg) {
var _this = this;
$.post('/api/notes/edit', {
id: this.id,
note: msg
}).done(function (res) {
if (res.status === 1) {
Toast(1, '更新成功!');
} else {
Toast(0, res.errorMsg);
}
});
},
/* 删除笔记 */
delete: function () {
var _this = this;
if (confirm("确认要删除吗?")) {
$.post('/api/notes/delete', {
id: this.id
}).done(function (res) {
if (res.status === 1) {
Toast(1, '删除成功!');
_this.$note.remove();
Event.fire('waterfall')
} else {
Toast(0, res.errorMsg);
}
});
}
}
}
5.笔记管理模块
var NoteManager = (function () {
//页面加载
function load() {
$.get('api/notes').done(function (res) {
if (res.status === 1) {
$.each(res.data, function (index, msg) {
new Note({
id: msg.id,
context: msg.text,
createTime: msg.createdAt.match(/^\d{4}-\d{1,2}-\d{1,2}/),
username: msg.username
});
});
Event.fire('waterfall');
} else {
Toast(0, res.errorMsg);
}
}).fail(function () {
Toast(0, "网络异常");
});
}
/* 添加笔记 */
function add() {
$.get('/login').then(function (res) {//判断是否登录
if (res.status === 1) {
new Note({
username: res.username
});
} else {
Toast(0, res.errorMsg);
}
});
}
return {
load: load,
add: add
}
})();
6.发布订阅模式
/* 发布订阅模式 */
var Event = (function () {
var events = {};
function on(evt, handler) {
events[evt] = events[evt] || [];
events[evt].push({
handler: handler
});
}
function fire(evt, args) {
if (!events[evt]) {
return;
}
for (var i = 0; i < events[evt].length; i++) {
events[evt][i].handler(args);
}
}
function off(name) {
delete events[name];
}
return {
on: on,
fire: fire,
off: off
}
})();
写完模块后,写入口文件index.js
require('sass/index.scss');
var Toast = require('mod/toast.js').Toast;
var WaterFall = require('mod/waterfall.js');
var NoteManager = require('mod/note-manager');
var Event = require('mod/event.js');
NoteManager.load();
$('.add-note').on('click', function () {
NoteManager.add();
})
Event.on('waterfall', function () {
WaterFall.init($("#content"));
})
到这就差不多完成了70%了,接下来就创建数据库,连接数据库了
/*创建数据库 运行 node note.js*/
var Sequelize = require('sequelize');
var path = require('path');
var sequelize = new Sequelize(undefined, undefined, undefined, {
host: 'localhost',
dialect: 'sqlite',
// SQLite only
storage: path.join(__dirname, '../database/database.sqlite')
});
/* 测试连接是否成功
node note.js
sequelize.authenticate()
.then(() => {
console.log('Connection has been established successfully.');
})
.catch(err => {
console.error('Unable to connect to the database:', err);
});
*/
var Note = sequelize.define('note', {
text: {
type: Sequelize.STRING
},
userid: {
type: Sequelize.INTEGER
},
username: {
type: Sequelize.STRING
}
});
Note.sync();
/*
删除表
Note.drop();
*/
/*
//创建数据库
Note.sync().then(function(){
Note.create({text:"sdsdsdsd"});
}).then(function(){
//查询表
Note.findAll({raw:true}).then(function(notes){
console.log(notes);
})
});
*/
module.exports = Note;
然后是在routes 里处理 ajax 请求,处理登录信息,获取 id,用户名等等,到这就基本完成了
总结
经过一星期的开发,了解了前后端联调,模块化开发方式、webpack 及loader和插件的使用、npm 的使用,Express的使用、路由、中间件、sqlite3、nodejs,在开发过程中还是有遇到许多问题,例如在连续声明变量的时候,不小心把逗号写成了分号,其他变量就变成了全局变量,于是就出错了,查了好久?
不过在这过程之中还是学到了许多,重要的是过程,继续往前端的路走下去?
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。