前端全栈实践



全栈开发的不同之处

开发环境也可以不用mock,直接提供后端server服务,这也是全栈开发和之前前端开发的不同之处。
可以把node后端当成中间层,请求旧后端并包装后返回。

本项目框架简介

项目使用webpack 1.15.0、vue 2.3.2、Node.js 6.9.2进行的开发。

使用模板vue-element-admin-boilerplate中关于开发的思想[1]进行布局,然后又丰富了它,包装成分为开发、生产模式的最外层的server.js。

使用vue-element-admin-boilerplate模板默认生成的开发服务器只是相当于当前项目中client文件夹内部client/build/dev-server.js

配置文件:.editorconfig、.eslintrc.js
项目介绍:readme.md。


效果

效果图

思路拆解

文件夹结构

| - client            前端
| - dist              打包目录,也是服务器的静态资源路径
| - node_modules      
| - server            后端服务器
| - static            client静态资源
| - package.json      工程文件
| - server.js         开发入口服务器

layout各部分功能分配

核心的部分有3个,最外层的server.js,client文件夹,server文件夹。
最外层的server.js是开发使用的。
client文件夹相当于原来的前端。
server文件夹相当于原来的后端。
node_modules前后端公用。
总体上:把前后端结合到一个项目中进行开发。

最外层server.js使用了express框架。

开发环境vs生产环境

不同之处

使用process.env.NODE_ENV(production为生产模式)并同时定义了一个变量useWebpackDev(false为生产模式)作为标志来判断是server.js是处于开发环境还是生产发布环境。
只有在npm run build的时候会将process.env.NODE_ENV设置为production(在client/build/build.js第4行)env.NODE_ENV = 'production',useWebpackDev是在server_config中设置的,而server_config又是依靠NODE_ENV来确定是引用important.js(生成环境配置)还是development.js,在important.js中useWebpackDev为false(或undefined),在development.js中useWebpackDev为true。
使用NODE_ENV之外再useWebpackDev可以给定义当前是什么环境多一些扩展性,因为:let env = process.env.NODE_ENV || 'development';,所以可以不用是非此即彼,即只能在development和production之间选择。
important.js其实内容是类似development.js,只不过是存在雨服务器上,避免保密数据泄露在git里。

相同之处

都是使用server.js提供服务,在生产环境也是当前的完整目录结构,依然是使用当前server.js提供服务。

开发环境和生产环境其实都是一样执行 node server.js,只不过其实分别用了nodemonpm2
开发环境使用webpack大礼包:webpack、webpack-dev-middleware、webpack-hot-middleware。
生产环境(已经build过)直接对根目录输出index.html(剩下的交给vue-router)及static服务。最后都是监听端口开启服务。

关于npm run build

npm run build命令主要是使用webpack打包以及一些复制粘贴工作(docker部署是另外一码事)。

开发环境

自己造server并同时使用webpack的思路:

1.利用express自己造了一个server服务
1.5 可能希望在这里提供一些路由服务(参考下面对路由劫持的分析)
2.app.use(require('connect-history-api-fallback')())包装req.url
3.var compiler = webpack(webpackConfig)定义了读取哪些怎么读取资源
4.dev-middleware读取资源
5.hot-middleware提供热刷新
5.5 可能希望在这里提供一些路由服务(参考下面对路由劫持的分析)

关于webpack-dev-server【我们项目没有使用,而是自己开发了一个server】

参考文献[2]
webpack-dev-server模块内部在node_modules中包含webpack-dev-midlleware(把webpack生成的compiler变成express中间件的容器)。

The webpack-dev-server is a little Node.js Express server, which uses the webpack-dev-middleware to serve a webpack bundle. It also has a little runtime which is connected to the server via Sock.js.

webpack-dev-server提供了项目目录文件的static服务,在模块源文件第203行:

app.get("*", express.static(contentBase), serveIndex(contentBase));

比如经过测试发现,使用dev-server的rainvue项目在浏览器能访问到根目录的任何文件。

关于express及其中间件

我们自己的server使用的是express。

关于middleware中间件看注解[3]。可以连续写多个函数,即多个连续执行的中间件。app.use([path,] callback [, callback...])

根部如果一样,就执行中间件,比如定义了path为/abc 则/abc/ddd也执行该中间件
app.use('/', function(){})app.use(function(){})没有区别,'/' (root path) 是默认值。

Mounts the specified middleware function or functions at the specified path: the middleware function is executed when the base of the requested path matches path.

express的错误处理中间件,必须包含4个参数,这是区别于普通中间件的标志。

app.use(function (err, req, res, next) {
  console.error(err.stack)
  res.status(500).send('Something broke!')
})

express.static是express内置中间件,提供静态资源服务。可以同时有多个static。不存在代码覆盖关系,匹配到哪一个就直接返回了,后面的也就不执行了。即中间件的本性:谁在前面谁生效。

//You can have more than one static directory per app:
app.use(express.static('public'))
app.use(express.static('uploads'))
app.use(express.static('files'))
// 本项目属于重复配置了static,可以后期清理下代码。
// server/index.js中 配置static服务路径是在生产环境中的路径
'/cms/getup/static' '/Users/liujunyang/henry/work/yktcms/server/dist/static'
// server.js中进入useWebpackDev的if后,也配置了static。
'/cms/getup/static' './static'
// else中也配置了static。
'/cms/getup/static' './dist/static'

关于connect-history-api-fallback

功能:在单页应用中,处理当刷新页面或直接在地址栏访问非根页面的时候,返回404的bug。匹配非文件(路径中不带.)的get请求。
connect-history-api-fallback会 包装 req.url。请求路径如果符合匹配规则(如/或路径如/dsfdsfd) 就会重写为/index.html(位于模块文件第71行),否则(如/test.do)就保持原样,最终都是进入next(),(模块文件的50-75行),继续执行后面的中间件。

...
if (parsedUrl.pathname.indexOf('.') !== -1 &&
    options.disableDotRule !== true) {
  logger(
    'Not rewriting',
    req.method,
    req.url,
    'because the path includes a dot (.) character.'
  );
  return next();
}

rewriteTarget = options.index || '/index.html';
logger('Rewriting', req.method, req.url, 'to', rewriteTarget);
req.url = rewriteTarget;
next();

关于webpack-dev-middleware中间件容器

1.生成的资源不是存在于文件夹,而是在内存中。

The webpack-dev-middleware is a small middleware for a connect-based middleware stack. It uses webpack to compile assets in-memory and serve them. When a compilation is running every request to the served webpack assets is blocked until we have a stable bundle.
*译:把资源编译进内存,在编译过程中,所有请求都不能进行,直到获得稳定的bundle。*

2.html-webpack-plugin只是提供渲染生成文件,至于生成到哪是根据compiler,比如webpack-dev-middleware 提供的服务是在内存中,则yktcms开发中,无论是生成index.html还是再生成别的文件,都是生成到内存中,在编辑器的文件目录中是看不到的。

3.webpack-dev-middleware提供的服务是利用compiler的配置编译在内存中的文件。
我们使用了html-webpack-plugin,dev模式开发时index.html不用server.js从views文件夹中进行render。所以即使把views中的index.html删除也不会影响dev模式开发。

new HtmlWebpackPlugin({
  filename: 'index.html',
  template: './client/index.html',
  inject: true
})

server在生产环境提供的views中的index.html是编译时通过npm run build生成的,并不是手动创建的。

4.webpack-dev-middleware 中有send,如果执行send返回了请求,就不会走express之后的中间件了。

function processRequest() {
	...

	// server content
	var content = context.fs.readFileSync(filename);
	content = shared.handleRangeHeaders(content, req, res);
	res.setHeader("Access-Control-Allow-Origin", "*"); // To support XHR, etc.
	res.setHeader("Content-Type", mime.lookup(filename) + "; charset=UTF-8");
	res.setHeader("Content-Length", content.length);
	if(context.options.headers) {
		for(var name in context.options.headers) {
			res.setHeader(name, context.options.headers[name]);
		}
	}
	// Express automatically sets the statusCode to 200, but not all servers do (Koa).
	res.statusCode = res.statusCode || 200;
	if(res.send) res.send(content);
	else res.end(content);
}

关于webpack-hot-middleware

为webpack提供热刷新功能。

Webpack hot reloading using only webpack-dev-middleware. This allows you to add hot reloading into an existing server without webpack-dev-server.

使用方法的伪代码:

Add the following plugins to the plugins array
Add 'webpack-hot-middleware/client' into the entry array.
app.use(require("webpack-hot-middleware")(compiler));

关于webpack的entry

这里说这个主要是介绍webpack-hot-middleware如何把 webpack-hot-middleware/client 加入entry。
entry单项如果是array,打包在一起输出最后一个。
webpack.dev.conf.js中的第9行:

baseWebpackConfig.entry[name] =['./client/build/dev-client'].concat(baseWebpackConfig.entry[name])
client/build/dev-client.js文件中的(require中省略了.js后缀)
'webpack-hot-middleware/client?reload=true’;其实是
'webpack-hot-middleware/client.js?reload=true';

关于walk

node-walk不是中间件,是一个遍历文件夹的nodejs库,只是遍历,遍历过程中想 要做什么处理的话给传入相应的函数即可。
比如我们是利用walk模块遍历mock文件夹的文件,让指定的路由(url)能返回访问指定的文件(require(mod)

...
var walk = require('walk')
var walker = walk.walk('./client/mock', {followLinks: false})
...
...
walker.on('file', function (root, fileStat, next) {
  if (!/.js$/.test(fileStat.name)) next()

  var filepath = path.join(root, fileStat.name)

  var url = filepath.replace('client/mock', '/mock').replace('.js', '')
  var mod = filepath.replace('client/mock', './client/mock')

  app.all(url, require(mod))

  next()
})
// 匹配的本地mock的url,带.do后缀是为了不匹配connect-history-api-fallback以及vue-router的路由规则
...
if (useMock) {
  api = {
  ...
    article: '/mock/article/article.do',
    article_create: '/mock/article/create.do',
    ...
  }
}
...

注意:walk一定不要忘记最后的next()。

一个请求进来之后的中间件处理顺序

可能到某一步就直接结束了,像过多层筛子,拦住了就不往下走了
1.如果符合放在前面的路由就直接结束。
2.connect-history-api-fallback。包装req.url。
3.webpack-dev-middleware的处理。

如果在内存中(即compiler生成的文件,如app.js,如index.html或htmlplugin生成的一系列html)输出req.url代表的文件,如/Users/liujunyang/henry/work/yktcms/dist/index.html。结束。

如果进入index.html,使用vue-router对当前路径再次进行匹配,劫持了所有的router,结束。不符合vue-router规则的才继续往下走next。
如果不在内存中,直接走next,也就不存在下面讨论的vue-router的过滤规则的事。

4.其他对文件的处理(能走到这的一般也就是文件了),比如上面介绍的walk的处理。

注意:在开发时,即useWebpackDev为true时,每个请求(不论文件还是接口)都会经过层层(不必然是所有的)中间件处理,所以即使是切换vue里面的路由其实也会把index.html本身再传输一遍。
而到线上之后,在输出index.html之后,凡是vue-router能匹配的请求都交给vue-router了。

vue-router过滤规则

1.vue-router.js的674-763行的代码是关于路径的正则匹配:凡是路径中带点.的路由【除xxx.html外,对xxx.html的处理和不带点的路径是一样的】,均不被vue-router匹配劫持。被匹配的就都会被vue-router处理(即劫持)。

2.在chrome的console中测试如下:

var PATH_REGEXP = new RegExp([
  // Match escaped characters that would otherwise appear in future matches.
  // This allows the user to escape special characters that won't transform.
  '(\\.)',
  // Match Express-style parameters and un-named parameters with a prefix
  // and optional suffixes. Matches appear as:
  //
  // "/:test(\d+)?" => ["/", "test", "d+", undefined, "?", undefined]
  // "/route(\d+)"  => [undefined, undefined, undefined, "d+", undefined, undefined]
  // "/*"            => ["/", undefined, undefined, undefined, undefined, "*"]
  '([\/.])?(?:(?:\:(\w+)(?:\(((?:\\.|[^\\()])+)\))?|\(((?:\\.|[^\\()])+)\))([+*?])?|(\*))'
].join('|'), 'g');

PATH_REGEXP
/(\.)|([/.])?(?:(?::(w+)(?:(((?:\.|[^\()])+)))?|(((?:\.|[^\()])+)))([+*?])?|(*))/g

PATH_REGEXP.test('http://localhost:20003/cms/getup/article-list2.dd')
true

PATH_REGEXP.exec('http://localhost:20003/cms/getup/article-list2.dd')
null

PATH_REGEXP.exec('http://localhost:20003/cms/getup/article-list2.')
null

PATH_REGEXP.test('http://localhost:20003/cms/getup/article-list2dd')
false

PATH_REGEXP.exec('http://localhost:20003/cms/getup/article-list2dd')
[":20003", undefined, undefined, "20003", undefined, undefined, undefined, undefined]

PATH_REGEXP.exec('http://localhost:20003/cms/getup/article-list2.html')
[":20003", undefined, undefined, "20003", undefined, undefined, undefined, undefined]

3.比如:在地址栏手动访问index.html,在webpack-dev-middleware输出index.html后,由于index.html页面中有app.js,app.js中安排了vue-router,那么接下来进入vue-router处理,由于路径 index.html能被vue-router的规则匹配,根据app.js的中路由的处理,fallback进入了404页面。

// fallback处理
routers.push({
  path: '*',
  name: 'notfound',
  component: require('./views/notfound')
})

但是如果有处理,就会进入相应的路由:

// 对路径 index.html匹配组件进行处理
{
    path: '/index.html',
    name: 'perm',
    component: require('./views/perm')
}

测试在地址栏输入一个路径后,我们的服务器是如何处理的

假设作为webpack entry的那个包含有vue-router的js文件为:main.js。
在html-webpack-plugin的规则中,inject为true时main.js注入html文件,否则html文件没有main.js文件。
1.第一个测试:
在htmlplugin中添加一个test.html,设置inject为false。在地址栏输入相应的pathto/test.html,回车。

流程是这样的:
因为test.html文件是htmlplugin中的,所以被编译进了内存中,webpack-dev-middleware输出test.html文件并经server返回给浏览器。
因为没有main.js,所以没有vue-router再对'pathto/test.html'进行处理,结束。
webpack-dev-middleware中处理的url为:'/Users/liujunyang/henry/work/yktcms/dist/test.html'

2.第二个测试:
在htmlplugin中添加一个test2.html,设置inject为true.在地址栏输入相应的pathto/test2.html,回车。

流程是这样的:
因为test2.html文件是htmlplugin中的,所以被编译进了内存中,webpack-dev-middleware输出test2.html文件并经server返回给浏览器。
因为有main.js,所以加载并执行main.js。
main.js中有vue-router,'pathto/test2.html'能被vue-router匹配,但是根据main.js中的路由配置,并没有对'pathto/test2.html'进行处理,就fallback进入了404页面(这个流程和上面vue-router过滤规则中第3条对index.html的介绍是一样的)。

3.第三个测试:
在地址栏输入pathto/test.do,回车(测试文件类型的路径)。

流程是这样的:
因为test.do不是htmlplugin中的,不在内存中,不会提前被webpack-dev-middleware输出。
中间件直接next(也就没有输出index.html,更是根本没vue-router什么事)。
然后根据server有没有提供路由服务或static服务,找到了就输出,没有找到的话就告知“Cannot GET /dd.do”

生产环境

概括

通过打包时npm run build之后生成的文件直接提供index.html服务,提供static服务。

没有connect-history-api-fallback
没有webpack-dev-middleware
没有webpack-hot-middleware
没有开发环境需要的mock(开发环境也可以不用mock,直接提供后端server服务,这也是全栈开发和之前前端开发的不同之处)。

部署

cms部署到雷

git pull
npm install
npm run build
pm2 restart cms
pm2 list

docker部署到雨

TODO 详解docker。

简单细节——解剖

概要

把webpack的配置文件放进了client端中,有历史原因(借鉴了vue-element-admin-boilerplate模板),其实是不应该的,应该放最外面。
涉及到的库、模式、工具等在下面的解剖中再逐一慢慢涉及。如redis、sequelize等。
下面的解剖都是简单解剖,详细细节再新开笔记进行单独介绍。本笔记着眼于整个项目的思路。

应用如何启动

如何启动开发环境 npm run dev

最外层server.js的解剖

基本上就是最外层布置了一个server。
根据环境判断条件决定进入哪个server。
开发环境需要devMiddleware。
生产环境由于是webpack打包好的,直接提供打包好的文件服务。

...
// server毛坯
const app = require('./server/index')
...
// 什么环境都需要的路由处理(放在了devMiddleware处理的前面)
app.use(client_config.ROOT_ROUTER + '/api', cms_router);
app.use(client_config.ROOT_ROUTER + '/preview', cms_router);
app.use(client_config.ROOT_ROUTER + '/front', front_router); 
...
if (process.env.NODE_ENV !== 'production' && useWebpackDev) {
...
// 开发环境
  var compiler = webpack(webpackConfig)
...
  app.use(require('connect-history-api-fallback')())
...
  app.use(devMiddleware)
...
  app.use(hotMiddleware)
...
} else {
...
// 生产环境
  app.use(client_config.ROOT_ROUTER, function (req, res, next) {   // 渲染cms页面
    res.render('index.html');
  });
...
}
...
app.listen(server_config.PORT)

client端的解剖

就是一个vue2.0项目。

layout

| - client          前端
  | - build           开发、生产配置文件
  | - config          配置文件
  | - mock            模拟数据
  | - src             前端vue开发
  | - index.html      vue项目的入口html文件

html结构

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no, minimal-ui">
    <meta name="format-detection" content="telephone=no" />
    <meta name="keywords" content="">
    <meta name="Description" content="">
    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="apple-mobile-web-app-status-bar-style" content="black">
    <link type="image/x-icon" rel="shortcut icon" href="">
    <title></title>
  </head>
  <body>
    <div id="app"></div>
    <script src="/cms/getup/static/js/tinymce/tinymce.min.js"></script>
    <script src="/cms/getup/static/js/qrcode.min.js"></script>
    <!-- built files will be auto injected -->
  </body>
</html>

入口js

client/src/main.js

import Vue from 'vue'
...
import VueRouter from 'vue-router'
import config from './config'
import App from './App'
...
Vue.use(VueRouter)
...
const router = new VueRouter({
  mode: 'history',
  base: config.ROOT_ROUTER,
  routes
})
...
/* eslint-disable no-new */
new Vue({
  el: '#app',
  template: '<App/>',
  router,
  components: {App}
})

根模板组件

client/src/App.vue

<template>
  <transition>
    <router-view></router-view>
  </transition>
</template>

路由控制

client/src/router-config.js

/* eslint-disable */
...
const routers = [
  { path: '/', component: require('./views/index') },
  {
    path: '/article',
    name: 'article',
    component: require('./views/article/index'),
    children: [
      {
        path: 'list',
        name: 'article-list',
        component: require('./views/article/list')
      },
      {
        path: 'create',
        name: 'article-editor',
        component: require('./views/article/editor')
      },
    ]
  },
  ...
]

routers.push({
  path: '*',
  name: 'notfound',
  component: require('./views/notfound')
})

export default routers

request处理函数

path: /client/src/api/request.js
使用axiosbluebird包装了ajax请求。

使用方法:

// '/client/src/api/article.js'
import request from './request'
import Api from './api'

export function createActivity (params) {
  return request.post(Api.activity_create, params)
}
...

server端的解剖

可以算是一个node项目。

layout

| - server            后端服务器
  | - config            配置
  | - controllers       处理业务逻辑
  | - helpers           工具方法
  | - models            数据库结构
  | - views             后端render的页面
  | - index.js          服务入口文件
  | - routes-front.js   路由:非cms
  | - routes_cmsapi.js  路由:cms

server端index.js

毛坯server。
使用express。

'use strict';
const express = require('express');
...
// 造server
const app = express();
// 模板引擎使用ejs
app.set('views', path.join(__dirname, 'views'));
app.engine('html', require('ejs').renderFile);
app.set('view engine', 'ejs');
// 定义静态资源static服务
app.use(client_config.ROOT_ROUTER + '/static', express.static(path.join(__dirname, 'dist/static')));

// 配置统一的header处理
app.use((req, res, next) => {
	res.header("Access-Control-Allow-Origin", '*');
	res.header("Access-Control-Allow-Headers", "User-Agent, Referer, Content-Type, Range, Content-Length, Authorization, Accept,X-Requested-With");
	res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");
	res.header("Access-Control-Max-Age", 24 * 60 * 60);//预检有效期
	res.header("Access-Control-Expose-Headers", "Content-Range");
	res.header("Access-Control-Allow-Credentials", true);
	if (req.method == "OPTIONS") return res.sendStatus(200);
...
    // 给每一个请求的res对象定义了一个格式化函数备用
	res.fmt = function (data) {		//格式化返回结果
		...
		return res.json(resData);
	};
	// 进入后续中间件处理
	next();
});
...
// 输出app在最外层server.js中继续进行包装。
module.exports = app;
...

流程

举2类例子:接口;渲染页面。
1.接口类以请求文章列表为例。
如某个ajax请求为pathto/api/article/list
根据server.js第35行app.use(client_config.ROOT_ROUTER + '/api', cms_router); ,被cms_router路由处理,其中cms_router为const cms_router = require('./server/routes_cmsapi');
cms_router路由中第74行router.get('/article/list', auth_validate, CmsArtileApiCtrl.articleList); 是对文章列表的处理。
其中 CmsArtileApiCtrlconst CmsArtileApiCtrl = require('./controllers/cms/article');,是对ArticleModel的操作的包装,所以放在controllers中,从数据库获取完数据后对接口进行返回:

...
}).then(function (result) {
	return res.fmt({
		data: {
			listData: result,
			count: count,
			pageNo: pageNo,
			pageSize: pageSize
		}
	});
})
...

ArticleModelconst ArticleModel = require('../../models').ArticleModel;,是用sequelize定义的数据库的用于文章的表。

2.渲染页面类以某篇文章页面为例。
如在浏览器地址栏请求为pathto/front/view/article/34回车。
根据server.js第37行app.use(client_config.ROOT_ROUTER + '/front', front_router); ,被front_router路由处理,其中front_router为const front_router = require('./server/routes-front');

front_router路由中第23行router.get('/view/article/:id', FrontArtileApiCtrl.frontViewArticle);是对文章的处理。
其中FrontArtileApiCtrlconst FrontArtileApiCtrl = require('./controllers/front/article');,是另外一个对ArticleModel的操作的包装,所以放在controllers中,是在构造函数的方法frontViewArticle中把对应id的数据从数据库获取完数据后套ejs模板并进行redis缓存后返回浏览器(如果redis中存在的话则直接返回已有的文章)。

ArticleModel跟上面第一类例子相同是const ArticleModel = require('../../models').ArticleModel;,是同一个用sequelize定义的数据库的用于文章的表。

路由配置 cms_router

对不同的接口请求配置不同的路由处理

...
const express = require('express');
...
const router = express.Router();
...
const CmsArtileApiCtrl = require('./controllers/cms/article');
...
//获取文章
router.get('/article', auth_validate, CmsArtileApiCtrl.article); 
//获取文章列表
router.get('/article/list', auth_validate, CmsArtileApiCtrl.articleList); 	
//创建文章
router.post('/article/create', auth_validate, CmsArtileApiCtrl.articleCreate); 	
//删除文章
router.post('/article/delete', auth_validate, CmsArtileApiCtrl.articleDelete); 
//编辑文章
router.post('/article/edit', auth_validate, CmsArtileApiCtrl.articleEdit); 	    	
...

module.exports = router;

包装数据库操作 CmsArtileApiCtrl

CmsArtileApiCtrl是一个构造函数,它的方法其实是作为router的中间件处理函数,进行的是数据库操作。

...
// 引用数据表实例
const ArticleModel = require('../../models').ArticleModel;
...
const redisArticleUpdate = require('../../helpers/ArtAndActCacheUpdate').redisArticleUpdate;
// 定义一个构造函数,它的方法其实是作为router的中间件处理函数
function Article() {
}
// 查询文章列表并返回 注意req, res, next
Article.prototype.articleList = function (req, res, next) {
	let where = {status: {'$ne': -1}};
    ...
	let count = 0;
	Promise.all([
	    // 进行真正的数据库操作
		ArticleModel.findAll(pagination_query),
		ArticleModel.count(count_query)
	]).then(function (result) {
		...
	}).then(function (result) {
		return res.fmt({
			data: {
				listData: result,
				count: count,
				pageNo: pageNo,
				pageSize: pageSize
			}
		});
	})
}
// 查询某篇文章并返回
Article.prototype.article = function (req, res, next) {
	let where = {id: req.query.id || 0};

	let query = {
		where: where
	}

	ArticleModel.findOne(query).then(function (result) {
		...
	}).then(function (result) {
		return res.fmt({
			status: 0,
			data: result
		});
	})
}

Article.prototype.articleCreate = function (req, res, next) {
	...
}
Article.prototype.articleEdit = function (req, res, next) {
	...
}
Article.prototype.articleDelete = function (req, res, next) {
	...
}
...
// 返回构造函数的实例
module.exports = new Article();

数据表 ArticleModel

依然是以本笔记中的文章的模型为例。
下面是使用sequelize建立的数据表的定义。定义了title, content等字段。
path: /server/models/article.js

// 小写 sequelize 的为我们创建的数据库实例,大写的 Sequelize 为使用的sequelize库模块
const Sequelize = require('../helpers/mysql').Sequelize;
const sequelize = require('../helpers/mysql').sequelize;
// 文章表
var CmsArticle = sequelize.define('cms_article', {
	title: {
		type: Sequelize.TEXT,
		allowNull: false,
		comment: '标题'
	},
	content: {
		type: Sequelize.TEXT,
		allowNull: false,
		comment: '内容'
	},
	...
}, {
	'createdAt': false,
	'updatedAt': false
});

// CmsArticle.sync({force: true})
module.exports = CmsArticle;

path: /server/helpers/mysql.js

'use strict';

const Sequelize = require('sequelize');
const mysql_config = require('../config/env').MYSQL[0];

const sequelize = new Sequelize(mysql_config.DATABASE, mysql_config.USER, mysql_config.PASSWORD, {
	host: mysql_config.HOST,
	dialect: 'mysql', //数据库类型
	pool: {
		max: 5,
		min: 0,
		idle: 10000
	},
	logging: false
});
// 小写 sequelize 的为我们创建的数据库实例,大写的 Sequelize 为使用的sequelize库模块
exports.sequelize = sequelize;
exports.Sequelize = Sequelize;

ejs模板

path: /server/views/article.html

<!DOCTYPE html>
<html>
  <head>
   ...
    <title><%= navigator_title %></title>
    
    <style>
      <% include ./common/css-article.html %>
      .wrapper {background: #fff;}
      ...
    </style>
    ...
  </head>
  <body>
    <div class="wrapper">
      ...
        <div><%- content %></div>
    ...
  </body>
</html>

redis处理

使用的ioredis
path: server/helpers/redis.js

完整layout

| - client          前端
  | - build           配置文件
    | - build.js              build时node调用的js
    | - config.js             一些公用配置
    | - webpack.base.conf.js  webpack基本配置
    | - webpack.dev.conf.js   webpack开发配置
    | - webpack.prod.conf.js  webpack生产配置
  | - config
    | - dev.env.js            开发环境NODE_ENV
    | - index.js              config模块的对外输出总接口
    | - prod.env.js           生产环境NODE_ENV
  | - mock            模拟数据
    | - article               文章相关
      | - list.do.js            文章列表的假数据
  | - src
    | - api                   定义请求接口
      | - api.js                定义接口的url(开发、生产)
      | - article.js            定义文章相关如何调用接口
      | - request.js            包装请求函数
    | - assets                静态资源
      | - loading.gif          
    | - components            存放定义的vue组件
      | - Editor.vue            富文本编辑组件
      | - Layout.vue            布局组件
      | - Search.vue            搜索框组件
      | - Editor.vue            富文本编辑组件
    | - helpers               存放公用js函数模块
    | - style                 存放公用scss模块(如mixin)
      | - _mixin.scss
    | - views                 存放路由页面
      | - article               文章相关页面
        | - editor.vue            编辑文章
        | - index.vue             router-view模板
        | - list.vue              文章列表
        | - preview.vue           预览文章
      | - index.vue             根页面
      | - login.vue             登录页面
      | - noauth.vue            无权限提示页面
      | - notfound.vue          404页面
    | - App.vue               根路由模板
    | - auth.js               权限管理模块
    | - config.js             开发配置
    | - main.js               vue入口js
    | - nav-config.js         导航栏路由配置
    | - router-config.js      路由配置
  | - index.html        vue项目的入口html文件
| - deploy            部署相关
| - dist              打包目录,也是服务器的静态资源路径
| - logs              自动生成的日志
| - node_modules      
| - scripts           node脚本
  | - job.js            生成字体、负责推送
| - server            后端服务器
  | - config            配置
    | - env               配置
      | - development.js    开发配置
      | - index.js          配置入口
      | - production.js     生产配置
    | - code.js           定义错误码
  | - controllers       处理业务逻辑
    | - cms               cms类
      | - article.js        文章处理接口,利用model操作数据库
      | - auth.js           权限过滤
    | - front             front类
      | - article.js         
  | - helpers           工具方法
    | - logger.js         日志处理
    | - mysql.js          mysql客户端
    | - permCache.js      权限相关
    | - redis.js          redis
    | - redisCacheUpdate.js redis
    | - request.js        包装请求
    | - upload.js         七牛上传
    | - userinfo.js       用户信息
    | - webfont.js        webfont
  | - models            数据库结构
    | - article.js        文章表
    | - index.js          入口
    | - profile.js        用户数据
  | - views             后端render的页面
    | - article.html      文章页ejs模板
    | - list.html         列表页ejs模板
  | - index.js          服务入口文件
  | - routes-front.js   路由:非cms
  | - routes_cmsapi.js  路由:cms
| - static            client静态资源
| - .bablerc          bablerc
| - .docerignore      docerignore
| - .editorconfig     editorconfig
| - .eslintignore     eslintignore
| - .eslintrc.js      eslintrc
| - .gitignore        gitignore
| - package.json      工程文件
| - readme.md         readme
| - server.js         开发服务器

  1. 并没有使用webpack的dev-server,而是自己提供了一套server开发环境,另外提供了一套后端服务的server,同时提供了一套前端的vue开发成果 ↩︎

  2. https://juejin.im/entry/574f95922e958a00683e402d
    http://acgtofe.com/posts/2016/02/full-live-reload-for-express-with-webpack
    https://github.com/bripkens/connect-history-api-fallback
    https://github.com/nuysoft/Mock/wiki
    http://fontawesome.io/icons/ ↩︎

  3. If the current middleware function does not end the request-response cycle, it must call next() to pass control to the next middleware function. Otherwise, the request will be left hanging.
    To skip the rest of the middleware functions from a router middleware stack, call next('route') to pass control to the next route. NOTE: next('route') will work only in middleware functions that were loaded by using the app.METHOD() or router.METHOD() functions. ↩︎

原文地址:https://www.cnblogs.com/liujunyang/p/7106030.html