前端全棧實踐



全棧開發的不一樣之處

開發環境也能夠不用mock,直接提供後端server服務,這也是全棧開發和以前前端開發的不一樣之處。
能夠把node後端當成中間層,請求舊後端幷包裝後返回。css

本項目框架簡介

項目使用webpack 1.15.0、vue 2.3.二、Node.js 6.9.2進行的開發。html

使用模板vue-element-admin-boilerplate中關於開發的思想1進行佈局,而後又豐富了它,包裝成分爲開發、生產模式的最外層的server.js。前端

使用vue-element-admin-boilerplate模板默認生成的開發服務器只是至關於當前項目中client文件夾內部client/build/dev-server.jsvue

配置文件:.editorconfig、.eslintrc.js
項目介紹:readme.md。java


效果

效果圖

思路拆解

文件夾結構

| - client            前端
| - dist              打包目錄,也是服務器的靜態資源路徑
| - node_modules      
| - server            後端服務器
| - static            client靜態資源
| - package.json      工程文件
| - server.js         開發入口服務器

layout各部分功能分配

核心的部分有3個,最外層的server.js,client文件夾,server文件夾。
最外層的server.js是開發使用的。
client文件夾至關於原來的前端。
server文件夾至關於原來的後端。
node_modules先後端公用。
整體上:把先後端結合到一個項目中進行開發。node

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

開發環境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裏。webpack

相同之處

都是使用server.js提供服務,在生產環境也是當前的完整目錄結構,依然是使用當前server.js提供服務。ios

開發環境和生產環境其實都是同樣執行 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.

相關文章
相關標籤/搜索