• 前端全栈实践




    全栈开发的不同之处

    开发环境也可以不用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. ↩︎

  • 相关阅读:
    jQuery事件
    jQuery的效果
    jQuery 选择器
    中级 jQuery 了解
    回调函数 callback()
    预加载
    表格对象的方法
    script中type属性讲解
    将数据渲染到页面的方式:模版
    将数据渲染到页面的几种方式
  • 原文地址:https://www.cnblogs.com/liujunyang/p/7106030.html
Copyright © 2020-2023  润新知