前端全棧實踐




全棧開發的不同之處

開發環境也可以不用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. ↩︎


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM