├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── README.md ├── build ├── build.js ├── check-versions.js ├── dev-client.js ├── dev-server.js ├── utils.js ├── webpack.base.conf.js ├── webpack.dev.conf.js └── webpack.prod.conf.js ├── config ├── dev.env.js ├── index.js ├── prod.env.js └── test.env.js ├── index.html ├── package.json ├── server ├── .env ├── .gitignore ├── api │ └── index.js ├── app.js ├── lib │ └── mongo.js ├── middleware │ ├── checkToken.js │ └── createToken.js ├── package.json └── routes │ ├── admin.js │ ├── article.js │ ├── classify.js │ ├── index.js │ ├── login.js │ └── reg.js ├── src ├── App.vue ├── api │ └── index.js ├── assets │ ├── css │ │ ├── commen.css │ │ ├── default.css │ │ ├── default0.css │ │ └── highlight.css │ ├── img │ │ ├── backend.gif │ │ ├── bg.jpg │ │ ├── bgm.jpg │ │ ├── fronted.gif │ │ └── mobile.gif │ └── js │ │ ├── commen.js │ │ ├── highlight.pack.js │ │ └── hljs.js ├── components │ ├── NotFound.vue │ ├── backEnd │ │ ├── Admin.vue │ │ ├── ArticleCreate.vue │ │ ├── ArticleEdit.vue │ │ ├── ArticleList.vue │ │ ├── ClassList.vue │ │ ├── Login.vue │ │ └── Reg.vue │ └── fronted │ │ ├── About.vue │ │ ├── Article.vue │ │ ├── Front.vue │ │ ├── Home.vue │ │ ├── Tags.vue │ │ ├── vfooter.vue │ │ └── vheader.vue ├── main.js ├── routes │ ├── index.js │ └── routes.js └── store │ ├── MsgAlert.js │ ├── actions.js │ ├── index.js │ ├── mutations.js │ ├── states.js │ └── types.js ├── static └── .gitkeep └── test ├── e2e ├── custom-assertions │ └── elementCount.js ├── nightwatch.conf.js ├── runner.js └── specs │ └── test.js └── unit ├── .eslintrc ├── index.js ├── karma.conf.js └── specs └── Hello.spec.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-2"], 3 | "plugins": ["transform-runtime"], 4 | "comments": false, 5 | "env": { 6 | "test": { 7 | "plugins": [ "istanbul" ] 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/*.js 2 | config/*.js 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: 'babel-eslint', 4 | parserOptions: { 5 | sourceType: 'module' 6 | }, 7 | // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style 8 | extends: 'standard', 9 | // required to lint *.vue files 10 | plugins: [ 11 | 'html' 12 | ], 13 | // add your custom rules here 14 | 'rules': { 15 | // allow paren-less arrow functions 16 | 'arrow-parens': 0, 17 | // allow async-await 18 | 'generator-star-spacing': 0, 19 | // allow debugger during development 20 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log 5 | test/unit/coverage 6 | test/e2e/reports 7 | selenium-debug.log 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > 一个前端基于Vue2.0全家桶,后端基于Express+Mongodb的前后端分离博客。前端界面使用了flexbox+rem布局,后端界面使用了element ui。本项目可以作为一个前端进阶项目,从前端flexbox布局到前端框架的使用,再到后端以及数据库,是一个打通前后端流程的一个项目。由于最近刚换了域名,正在备案中,所以目前没有线上演示,不过可以看一下下面的动态图特效。 2 | 3 | # 特点 4 | * 支持 MarkDown 编辑 5 | * 支持代码高亮 6 | * 支持移动端浏览 7 | 8 | [在线地址](http://www.dailu.site) 9 | 10 | # 功能展示展示 11 | 12 | 13 | #### PC前台博客演示 14 | 15 | ![](src/assets/img/fronted.gif) 16 | 17 | 18 | #### 移动端前台演示 19 | ![](src/assets/img/mobile.gif) 20 | 21 | #### 后台管理演示 22 | ![](src/assets/img/backend.gif) 23 | 24 | #### 若图片无法显示则点击这里:[pc前端功能展示](http://ofyxyx8o9.bkt.clouddn.com/fronted.gif) [移动前端功能展示](http://ofyxyx8o9.bkt.clouddn.com/mobile.gif)[pc后端功能展示](http://ofyxyx8o9.bkt.clouddn.com/backend.gif) 25 | 26 | ### 前端工具 27 | * Vue2.0 28 | * Vue-Router 29 | * Vuex 30 | * axios 31 | * element ui 32 | 33 | 前端布局采用flexbox+rem布局,关于flexbox请阅读[一个完整的Flexbox指南](http://www.w3cplus.com/css3/a-guide-to-flexbox-new.html)以及这篇最新的[理解Flexbox:你需要知道的一切](http://www.w3cplus.com/css3/understanding-flexbox-everything-you-need-to-know.html) 34 | 35 | 本项目还采用了手机端适配,关于移动端的学习资料请按照我下面罗列的资料按顺序仔细阅读。 36 | 37 | 1. [移动端调试](https://segmentfault.com/a/1190000002565572) 38 | 2. [介绍vieport](http://www.css88.com/archives/6410) 39 | 4. [使用Flexible实现手淘H5页面的终端适配](https://github.com/amfe/article/issues/17) 40 | 41 | 42 | 43 | ### 后端工具 44 | * express 45 | * mongodb(mongolass) 46 | 47 | 后端的管理界面我直接使用了[element ui](http://element.eleme.io/#/)这个基于vue的组件库,很强大. 48 | 49 | 50 | # 目录结构 51 | 52 | ``` 53 | │ .babelrc 54 | │ .editorconfig 55 | │ .eslintignore 56 | │ .eslintrc.js 57 | │ .gitignore 58 | │ index.html 59 | │ package.json 60 | │ README.md 61 | │ 62 | ├─build 63 | │ build.js 64 | │ check-versions.js 65 | │ dev-client.js 66 | │ dev-server.js 67 | │ utils.js 68 | │ webpack.base.conf.js 69 | │ webpack.dev.conf.js 70 | │ webpack.prod.conf.js 71 | │ 72 | ├─config 73 | │ dev.env.js 74 | │ index.js 75 | │ prod.env.js 76 | │ test.env.js 77 | │ 78 | ├─server 后端文件夹 79 | │ │ .env 80 | │ │ app.js 后端入口 81 | │ │ 82 | │ ├─api 83 | │ │ index.js 后端api 84 | │ │ 85 | │ ├─lib 86 | │ │ mongo.js 数据库 87 | │ │ 88 | │ ├─middleware 89 | │ │ checkToken.js 90 | │ │ createToken.js 91 | │ │ 92 | │ └─routes 后端路由 93 | │ admin.js 94 | │ article.js 95 | │ classify.js 96 | │ index.js 97 | │ login.js 98 | │ reg.js 99 | │ 100 | ├─src 前端文件夹 101 | │ │ App.vue 102 | │ │ main.js 前端入口 103 | │ │ 104 | │ ├─api 前端api 105 | │ │ index.js 106 | │ │ 107 | │ ├─assets 108 | │ │ ├─css 109 | │ │ │ commen.css 110 | │ │ │ default.css 111 | │ │ │ default0.css 112 | │ │ │ highlight.css 113 | │ │ │ 114 | │ │ ├─img 115 | │ │ │ bg.jpg 116 | │ │ │ bgm.jpg 117 | │ │ │ 118 | │ │ └─js 119 | │ │ commen.js 120 | │ │ highlight.pack.js 121 | │ │ hljs.js 122 | │ │ 123 | │ ├─components 124 | │ │ │ NotFound.vue 125 | │ │ │ 126 | │ │ ├─backEnd 后端界面 127 | │ │ │ Admin.vue 128 | │ │ │ ArticleCreate.vue 129 | │ │ │ ArticleEdit.vue 130 | │ │ │ ArticleList.vue 131 | │ │ │ ClassList.vue 132 | │ │ │ Login.vue 133 | │ │ │ Reg.vue 134 | │ │ │ 135 | │ │ └─fronted 前端界面 136 | │ │ About.vue 137 | │ │ Article.vue 138 | │ │ Front.vue 139 | │ │ Home.vue 140 | │ │ Tags.vue 141 | │ │ vfooter.vue 142 | │ │ vheader.vue 143 | │ │ 144 | │ ├─routes 前端路由vue-router 145 | │ │ index.js 146 | │ │ routes.js 147 | │ │ 148 | │ └─store 前端vuex 149 | │ │ actions.js 150 | │ │ index.js 151 | │ │ MsgAlert.js 152 | │ │ mutations.js 153 | │ │ states.js 154 | │ │ types.js 155 | ``` 156 | 157 | ## Step 158 | #### 环境 159 | * Node.js v6 160 | * mongoDB [下载地址](https://www.mongodb.com/download-center?jmp=nav#community) 161 | [安装方法](https://docs.mongodb.com/manual/installation/) 162 | 安装完成后启动数据库 163 | `mongod` 164 | 165 | 166 | #### 克隆远程库 167 | `git clone https://github.com/elva2596/vueBlog.git` 168 | 169 | 170 | #### 安装前端依赖 171 | `npm install` 172 | 173 | 174 | #### 全局安装supervisor 175 | `npm install -g supervisor` 176 | 177 | 178 | #### 启动mongodb服务器 179 | 180 | 在你安装的数据库文件中的bin目录中启动 181 | 182 | `mongod` 183 | 184 | #### 安装后端依赖 185 | `进入到server文件夹下,安装后端依赖:npm install` 186 | 187 | #### 启动后端服务器 188 | `npm start` 189 | 190 | 191 | #### 启动前端项目 192 | `回到项目根目录下运行:npm run dev` 193 | 194 | 195 | #### 生成发布 196 | `npm run build` 197 | 198 | 199 | #### Notice 200 | * 前后端启动时的路径不一样,前端在项目根目录,后端在servser根目录,其实可以把后端分离出来。 201 | * 因为是前后端分离项目,必然涉及到跨域,使用webpack的proxyTable,进入到config文件夹的index.js,将proxyTable配置成: 202 | `proxyTable: { 203 | '/api':{ 204 | target:'http://localhost:3009/api', 205 | changeOrigin:true, 206 | pathRewrite:{ 207 | '^/api':'' 208 | } 209 | } 210 | },` 211 | 212 | 213 | 214 | * 推荐[Robomongo](https://robomongo.org/)作为数据库的可视化管理工具 215 | * 推荐Postman作为验证restful API的工具,请参考[这篇文章](http://www.cnblogs.com/mafly/p/postman.html) 216 | * 这里的主页界面有个小小的bug,主页只显示每篇具体文章的摘要,在这里我取了一个巧,就是将从后端返回的数据经过markdown解析以后,用了一个正则把第一个p标签以及其中的内容给提取出来渲染到页面。因此后端在添加文章的时候必须在编写每篇文章的开头写一段话。因为毕竟这个博客只是一个第一版,后面我会把这个问题解决掉。 217 | 218 | 219 | # Todo 220 | * 移动端优化,300ms 221 | * 添加评论功能 222 | * 密码修改功能 223 | * 用户权限分类 224 | 225 | 226 | ## License 227 | MIT 228 | -------------------------------------------------------------------------------- /build/build.js: -------------------------------------------------------------------------------- 1 | // https://github.com/shelljs/shelljs 2 | require('./check-versions')() 3 | require('shelljs/global') 4 | env.NODE_ENV = 'production' 5 | 6 | var path = require('path') 7 | var config = require('../config') 8 | var ora = require('ora') 9 | var webpack = require('webpack') 10 | var webpackConfig = require('./webpack.prod.conf') 11 | 12 | console.log( 13 | ' Tip:\n' + 14 | ' Built files are meant to be served over an HTTP server.\n' + 15 | ' Opening index.html over file:// won\'t work.\n' 16 | ) 17 | 18 | var spinner = ora('building for production...') 19 | spinner.start() 20 | 21 | var assetsPath = path.join(config.build.assetsRoot, config.build.assetsSubDirectory) 22 | rm('-rf', assetsPath) 23 | mkdir('-p', assetsPath) 24 | cp('-R', 'static/*', assetsPath) 25 | 26 | webpack(webpackConfig, function (err, stats) { 27 | spinner.stop() 28 | if (err) throw err 29 | process.stdout.write(stats.toString({ 30 | colors: true, 31 | modules: false, 32 | children: false, 33 | chunks: false, 34 | chunkModules: false 35 | }) + '\n') 36 | }) 37 | -------------------------------------------------------------------------------- /build/check-versions.js: -------------------------------------------------------------------------------- 1 | var semver = require('semver') 2 | var chalk = require('chalk') 3 | var packageConfig = require('../package.json') 4 | var exec = function (cmd) { 5 | return require('child_process') 6 | .execSync(cmd).toString().trim() 7 | } 8 | 9 | var versionRequirements = [ 10 | { 11 | name: 'node', 12 | currentVersion: semver.clean(process.version), 13 | versionRequirement: packageConfig.engines.node 14 | }, 15 | { 16 | name: 'npm', 17 | currentVersion: exec('npm --version'), 18 | versionRequirement: packageConfig.engines.npm 19 | } 20 | ] 21 | 22 | module.exports = function () { 23 | var warnings = [] 24 | for (var i = 0; i < versionRequirements.length; i++) { 25 | var mod = versionRequirements[i] 26 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { 27 | warnings.push(mod.name + ': ' + 28 | chalk.red(mod.currentVersion) + ' should be ' + 29 | chalk.green(mod.versionRequirement) 30 | ) 31 | } 32 | } 33 | 34 | if (warnings.length) { 35 | console.log('') 36 | console.log(chalk.yellow('To use this template, you must update following to modules:')) 37 | console.log() 38 | for (var i = 0; i < warnings.length; i++) { 39 | var warning = warnings[i] 40 | console.log(' ' + warning) 41 | } 42 | console.log() 43 | process.exit(1) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /build/dev-client.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | require('eventsource-polyfill') 3 | var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true') 4 | 5 | hotClient.subscribe(function (event) { 6 | if (event.action === 'reload') { 7 | window.location.reload() 8 | } 9 | }) 10 | -------------------------------------------------------------------------------- /build/dev-server.js: -------------------------------------------------------------------------------- 1 | require('./check-versions')() 2 | var config = require('../config') 3 | if (!process.env.NODE_ENV) process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV) 4 | var path = require('path') 5 | var express = require('express') 6 | var webpack = require('webpack') 7 | var opn = require('opn') 8 | var proxyMiddleware = require('http-proxy-middleware') 9 | var webpackConfig = process.env.NODE_ENV === 'testing' 10 | ? require('./webpack.prod.conf') 11 | : require('./webpack.dev.conf') 12 | 13 | // default port where dev server listens for incoming traffic 14 | var port = process.env.PORT || config.dev.port 15 | // Define HTTP proxies to your custom API backend 16 | // https://github.com/chimurai/http-proxy-middleware 17 | var proxyTable = config.dev.proxyTable 18 | 19 | var app = express() 20 | var compiler = webpack(webpackConfig) 21 | 22 | var devMiddleware = require('webpack-dev-middleware')(compiler, { 23 | publicPath: webpackConfig.output.publicPath, 24 | quiet: true 25 | }) 26 | 27 | var hotMiddleware = require('webpack-hot-middleware')(compiler, { 28 | log: () => {} 29 | }) 30 | // force page reload when html-webpack-plugin template changes 31 | compiler.plugin('compilation', function (compilation) { 32 | compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) { 33 | hotMiddleware.publish({ action: 'reload' }) 34 | cb() 35 | }) 36 | }) 37 | 38 | // proxy api requests 39 | Object.keys(proxyTable).forEach(function (context) { 40 | var options = proxyTable[context] 41 | if (typeof options === 'string') { 42 | options = { target: options } 43 | } 44 | app.use(proxyMiddleware(context, options)) 45 | }) 46 | 47 | // handle fallback for HTML5 history API 48 | app.use(require('connect-history-api-fallback')()) 49 | 50 | // serve webpack bundle output 51 | app.use(devMiddleware) 52 | 53 | // enable hot-reload and state-preserving 54 | // compilation error display 55 | app.use(hotMiddleware) 56 | 57 | // serve pure static assets 58 | var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory) 59 | app.use(staticPath, express.static('./static')) 60 | 61 | var uri = 'http://localhost:' + port 62 | 63 | devMiddleware.waitUntilValid(function () { 64 | console.log('> Listening at ' + uri + '\n') 65 | }) 66 | 67 | module.exports = app.listen(port, function (err) { 68 | if (err) { 69 | console.log(err) 70 | return 71 | } 72 | 73 | // when env is testing, don't need open it 74 | if (process.env.NODE_ENV !== 'testing') { 75 | opn(uri) 76 | } 77 | }) 78 | -------------------------------------------------------------------------------- /build/utils.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var config = require('../config') 3 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 4 | 5 | exports.assetsPath = function (_path) { 6 | var assetsSubDirectory = process.env.NODE_ENV === 'production' 7 | ? config.build.assetsSubDirectory 8 | : config.dev.assetsSubDirectory 9 | return path.posix.join(assetsSubDirectory, _path) 10 | } 11 | 12 | exports.cssLoaders = function (options) { 13 | options = options || {} 14 | // generate loader string to be used with extract text plugin 15 | function generateLoaders (loaders) { 16 | var sourceLoader = loaders.map(function (loader) { 17 | var extraParamChar 18 | if (/\?/.test(loader)) { 19 | loader = loader.replace(/\?/, '-loader?') 20 | extraParamChar = '&' 21 | } else { 22 | loader = loader + '-loader' 23 | extraParamChar = '?' 24 | } 25 | return loader + (options.sourceMap ? extraParamChar + 'sourceMap' : '') 26 | }).join('!') 27 | 28 | // Extract CSS when that option is specified 29 | // (which is the case during production build) 30 | if (options.extract) { 31 | return ExtractTextPlugin.extract('vue-style-loader', sourceLoader) 32 | } else { 33 | return ['vue-style-loader', sourceLoader].join('!') 34 | } 35 | } 36 | 37 | // http://vuejs.github.io/vue-loader/en/configurations/extract-css.html 38 | return { 39 | css: generateLoaders(['css']), 40 | postcss: generateLoaders(['css']), 41 | less: generateLoaders(['css', 'less']), 42 | sass: generateLoaders(['css', 'sass?indentedSyntax']), 43 | scss: generateLoaders(['css', 'sass']), 44 | stylus: generateLoaders(['css', 'stylus']), 45 | styl: generateLoaders(['css', 'stylus']) 46 | } 47 | } 48 | 49 | // Generate loaders for standalone style files (outside of .vue) 50 | exports.styleLoaders = function (options) { 51 | var output = [] 52 | var loaders = exports.cssLoaders(options) 53 | for (var extension in loaders) { 54 | var loader = loaders[extension] 55 | output.push({ 56 | test: new RegExp('\\.' + extension + '$'), 57 | loader: loader 58 | }) 59 | } 60 | return output 61 | } 62 | -------------------------------------------------------------------------------- /build/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var config = require('../config') 3 | var utils = require('./utils') 4 | var projectRoot = path.resolve(__dirname, '../') 5 | 6 | var env = process.env.NODE_ENV 7 | // check env & config/index.js to decide whether to enable CSS source maps for the 8 | // various preprocessor loaders added to vue-loader at the end of this file 9 | var cssSourceMapDev = (env === 'development' && config.dev.cssSourceMap) 10 | var cssSourceMapProd = (env === 'production' && config.build.productionSourceMap) 11 | var useCssSourceMap = cssSourceMapDev || cssSourceMapProd 12 | 13 | module.exports = { 14 | entry: { 15 | app: './src/main.js' 16 | }, 17 | output: { 18 | path: config.build.assetsRoot, 19 | publicPath: process.env.NODE_ENV === 'production' ? config.build.assetsPublicPath : config.dev.assetsPublicPath, 20 | filename: '[name].js' 21 | }, 22 | resolve: { 23 | extensions: ['', '.js', '.vue', '.json'], 24 | fallback: [path.join(__dirname, '../node_modules')], 25 | alias: { 26 | 'vue$': 'vue/dist/vue.common.js', 27 | 'src': path.resolve(__dirname, '../src'), 28 | 'assets': path.resolve(__dirname, '../src/assets'), 29 | 'components': path.resolve(__dirname, '../src/components') 30 | } 31 | }, 32 | resolveLoader: { 33 | fallback: [path.join(__dirname, '../node_modules')] 34 | }, 35 | module: { 36 | // preLoaders: [ 37 | // { 38 | // test: /\.vue$/, 39 | // loader: 'eslint', 40 | // include: [ 41 | // path.join(projectRoot, 'src') 42 | // ], 43 | // exclude: /node_modules/ 44 | // }, 45 | // { 46 | // test: /\.js$/, 47 | // loader: 'eslint', 48 | // include: [ 49 | // path.join(projectRoot, 'src') 50 | // ], 51 | // exclude: /node_modules/ 52 | // } 53 | // ], 54 | loaders: [ 55 | { 56 | test: /\.vue$/, 57 | loader: 'vue' 58 | }, 59 | { 60 | test: /\.js$/, 61 | loader: 'babel', 62 | include: [ 63 | path.join(projectRoot, 'src') 64 | ], 65 | exclude: /node_modules/ 66 | }, 67 | { 68 | test: /\.json$/, 69 | loader: 'json' 70 | }, 71 | { 72 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 73 | loader: 'url', 74 | query: { 75 | limit: 10000, 76 | name: utils.assetsPath('img/[name].[hash:7].[ext]') 77 | } 78 | }, 79 | { 80 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 81 | loader: 'url', 82 | query: { 83 | limit: 10000, 84 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]') 85 | } 86 | } 87 | ] 88 | }, 89 | eslint: { 90 | formatter: require('eslint-friendly-formatter') 91 | }, 92 | vue: { 93 | loaders: utils.cssLoaders({ sourceMap: useCssSourceMap }), 94 | postcss: [ 95 | require('autoprefixer')({ 96 | browsers: ['last 2 versions'] 97 | }) 98 | ] 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | var config = require('../config') 2 | var webpack = require('webpack') 3 | var merge = require('webpack-merge') 4 | var utils = require('./utils') 5 | var baseWebpackConfig = require('./webpack.base.conf') 6 | var HtmlWebpackPlugin = require('html-webpack-plugin') 7 | var FriendlyErrors = require('friendly-errors-webpack-plugin') 8 | 9 | // add hot-reload related code to entry chunks 10 | Object.keys(baseWebpackConfig.entry).forEach(function (name) { 11 | baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name]) 12 | }) 13 | 14 | module.exports = merge(baseWebpackConfig, { 15 | module: { 16 | loaders: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap }) 17 | }, 18 | // eval-source-map is faster for development 19 | devtool: '#eval-source-map', 20 | plugins: [ 21 | new webpack.DefinePlugin({ 22 | 'process.env': config.dev.env 23 | }), 24 | // https://github.com/glenjamin/webpack-hot-middleware#installation--usage 25 | new webpack.optimize.OccurrenceOrderPlugin(), 26 | new webpack.HotModuleReplacementPlugin(), 27 | new webpack.NoErrorsPlugin(), 28 | // https://github.com/ampedandwired/html-webpack-plugin 29 | new HtmlWebpackPlugin({ 30 | filename: 'index.html', 31 | template: 'index.html', 32 | inject: true 33 | }), 34 | new FriendlyErrors() 35 | ] 36 | }) 37 | -------------------------------------------------------------------------------- /build/webpack.prod.conf.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var config = require('../config') 3 | var utils = require('./utils') 4 | var webpack = require('webpack') 5 | var merge = require('webpack-merge') 6 | var baseWebpackConfig = require('./webpack.base.conf') 7 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 8 | var HtmlWebpackPlugin = require('html-webpack-plugin') 9 | var env = process.env.NODE_ENV === 'testing' 10 | ? require('../config/test.env') 11 | : config.build.env 12 | 13 | var webpackConfig = merge(baseWebpackConfig, { 14 | module: { 15 | loaders: utils.styleLoaders({ sourceMap: config.build.productionSourceMap, extract: true }) 16 | }, 17 | devtool: config.build.productionSourceMap ? '#source-map' : false, 18 | output: { 19 | path: config.build.assetsRoot, 20 | filename: utils.assetsPath('js/[name].[chunkhash].js'), 21 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') 22 | }, 23 | vue: { 24 | loaders: utils.cssLoaders({ 25 | sourceMap: config.build.productionSourceMap, 26 | extract: true 27 | }) 28 | }, 29 | plugins: [ 30 | // http://vuejs.github.io/vue-loader/en/workflow/production.html 31 | new webpack.DefinePlugin({ 32 | 'process.env': env 33 | }), 34 | new webpack.optimize.UglifyJsPlugin({ 35 | compress: { 36 | warnings: false 37 | } 38 | }), 39 | new webpack.optimize.OccurrenceOrderPlugin(), 40 | // extract css into its own file 41 | new ExtractTextPlugin(utils.assetsPath('css/[name].[contenthash].css')), 42 | // generate dist index.html with correct asset hash for caching. 43 | // you can customize output by editing /index.html 44 | // see https://github.com/ampedandwired/html-webpack-plugin 45 | new HtmlWebpackPlugin({ 46 | filename: process.env.NODE_ENV === 'testing' 47 | ? 'index.html' 48 | : config.build.index, 49 | template: 'index.html', 50 | inject: true, 51 | minify: { 52 | removeComments: true, 53 | collapseWhitespace: true, 54 | removeAttributeQuotes: true 55 | // more options: 56 | // https://github.com/kangax/html-minifier#options-quick-reference 57 | }, 58 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin 59 | chunksSortMode: 'dependency' 60 | }), 61 | // split vendor js into its own file 62 | new webpack.optimize.CommonsChunkPlugin({ 63 | name: 'vendor', 64 | minChunks: function (module, count) { 65 | // any required modules inside node_modules are extracted to vendor 66 | return ( 67 | module.resource && 68 | /\.js$/.test(module.resource) && 69 | module.resource.indexOf( 70 | path.join(__dirname, '../node_modules') 71 | ) === 0 72 | ) 73 | } 74 | }), 75 | // extract webpack runtime and module manifest to its own file in order to 76 | // prevent vendor hash from being updated whenever app bundle is updated 77 | new webpack.optimize.CommonsChunkPlugin({ 78 | name: 'manifest', 79 | chunks: ['vendor'] 80 | }) 81 | ] 82 | }) 83 | 84 | if (config.build.productionGzip) { 85 | var CompressionWebpackPlugin = require('compression-webpack-plugin') 86 | 87 | webpackConfig.plugins.push( 88 | new CompressionWebpackPlugin({ 89 | asset: '[path].gz[query]', 90 | algorithm: 'gzip', 91 | test: new RegExp( 92 | '\\.(' + 93 | config.build.productionGzipExtensions.join('|') + 94 | ')$' 95 | ), 96 | threshold: 10240, 97 | minRatio: 0.8 98 | }) 99 | ) 100 | } 101 | 102 | module.exports = webpackConfig 103 | -------------------------------------------------------------------------------- /config/dev.env.js: -------------------------------------------------------------------------------- 1 | var merge = require('webpack-merge') 2 | var prodEnv = require('./prod.env') 3 | 4 | module.exports = merge(prodEnv, { 5 | NODE_ENV: '"development"' 6 | }) 7 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | // see http://vuejs-templates.github.io/webpack for documentation. 2 | var path = require('path') 3 | 4 | module.exports = { 5 | build: { 6 | env: require('./prod.env'), 7 | index: path.resolve(__dirname, '../dist/index.html'), 8 | assetsRoot: path.resolve(__dirname, '../dist'), 9 | assetsSubDirectory: 'static', 10 | assetsPublicPath: '/', 11 | productionSourceMap: false, 12 | // Gzip off by default as many popular static hosts such as 13 | // Surge or Netlify already gzip all static assets for you. 14 | // Before setting to `true`, make sure to: 15 | // npm install --save-dev compression-webpack-plugin 16 | productionGzip: false, 17 | productionGzipExtensions: ['js', 'css'] 18 | }, 19 | dev: { 20 | env: require('./dev.env'), 21 | port: 8088, 22 | assetsSubDirectory: 'static', 23 | assetsPublicPath: '/', 24 | proxyTable: { 25 | '/api':{ 26 | target:'http://127.0.0.1:3009/api', 27 | changeOrigin:true, 28 | pathRewrite:{ 29 | '^/api':'' 30 | } 31 | } 32 | }, 33 | // CSS Sourcemaps off by default because relative paths are "buggy" 34 | // with this option, according to the CSS-Loader README 35 | // (https://github.com/webpack/css-loader#sourcemaps) 36 | // In our experience, they generally work as expected, 37 | // just be aware of this issue when enabling this option. 38 | cssSourceMap: false 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /config/prod.env.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | NODE_ENV: '"production"' 3 | } 4 | -------------------------------------------------------------------------------- /config/test.env.js: -------------------------------------------------------------------------------- 1 | var merge = require('webpack-merge') 2 | var devEnv = require('./dev.env') 3 | 4 | module.exports = merge(devEnv, { 5 | NODE_ENV: '"testing"' 6 | }) 7 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 平凡之路的博客 7 | 8 | 9 |
10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "myblog", 3 | "version": "1.0.0", 4 | "description": "A Vue.js project", 5 | "author": "", 6 | "private": true, 7 | "scripts": { 8 | "dev": "node build/dev-server.js", 9 | "build": "node build/build.js", 10 | "unit": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js --single-run", 11 | "e2e": "node test/e2e/runner.js", 12 | "test": "npm run unit && npm run e2e", 13 | "lint": "eslint --ext .js,.vue src test/unit/specs test/e2e/specs" 14 | }, 15 | "dependencies": { 16 | "axios": "^0.15.3", 17 | "body-parser": "^1.16.0", 18 | "dotenv": "^2.0.0", 19 | "element-ui": "^1.1.6", 20 | "express": "^4.14.1", 21 | "express-jwt": "^5.1.0", 22 | "highlight.js": "^9.9.0", 23 | "jsonwebtoken": "^7.2.1", 24 | "marked": "^0.3.6", 25 | "moment": "^2.17.1", 26 | "mongolass": "^2.4.1", 27 | "nprogress": "^0.2.0", 28 | "objectid-to-timestamp": "^1.3.0", 29 | "sha1": "^1.1.1", 30 | "store2": "^2.5.0", 31 | "vue": "^2.1.0", 32 | "vue-router": "^2.2.0", 33 | "vuex": "^2.1.1" 34 | }, 35 | "devDependencies": { 36 | "autoprefixer": "^6.4.0", 37 | "axios-mock-adapter": "^1.7.1", 38 | "babel-core": "^6.0.0", 39 | "babel-eslint": "^7.0.0", 40 | "babel-loader": "^6.0.0", 41 | "babel-plugin-istanbul": "^3.0.0", 42 | "babel-plugin-transform-runtime": "^6.0.0", 43 | "babel-preset-es2015": "^6.0.0", 44 | "babel-preset-stage-2": "^6.0.0", 45 | "babel-register": "^6.0.0", 46 | "chai": "^3.5.0", 47 | "chalk": "^1.1.3", 48 | "chromedriver": "^2.21.2", 49 | "connect-history-api-fallback": "^1.1.0", 50 | "cross-env": "^3.1.3", 51 | "cross-spawn": "^4.0.2", 52 | "css-loader": "^0.25.0", 53 | "eslint": "^3.7.1", 54 | "eslint-config-standard": "^6.1.0", 55 | "eslint-friendly-formatter": "^2.0.5", 56 | "eslint-loader": "^1.5.0", 57 | "eslint-plugin-html": "^1.3.0", 58 | "eslint-plugin-promise": "^3.4.0", 59 | "eslint-plugin-standard": "^2.0.1", 60 | "eventsource-polyfill": "^0.9.6", 61 | "express": "^4.13.3", 62 | "extract-text-webpack-plugin": "^1.0.1", 63 | "file-loader": "^0.9.0", 64 | "friendly-errors-webpack-plugin": "^1.1.2", 65 | "function-bind": "^1.0.2", 66 | "html-webpack-plugin": "^2.8.1", 67 | "http-proxy-middleware": "^0.17.2", 68 | "inject-loader": "^2.0.1", 69 | "json-loader": "^0.5.4", 70 | "karma": "^1.3.0", 71 | "karma-coverage": "^1.1.1", 72 | "karma-mocha": "^1.2.0", 73 | "karma-phantomjs-launcher": "^1.0.0", 74 | "karma-sinon-chai": "^1.2.0", 75 | "karma-sourcemap-loader": "^0.3.7", 76 | "karma-spec-reporter": "0.0.26", 77 | "karma-webpack": "^1.7.0", 78 | "lolex": "^1.4.0", 79 | "mocha": "^3.1.0", 80 | "mockjs": "^1.0.1-beta3", 81 | "nightwatch": "^0.9.8", 82 | "opn": "^4.0.2", 83 | "ora": "^0.3.0", 84 | "phantomjs-prebuilt": "^2.1.3", 85 | "selenium-server": "2.53.1", 86 | "semver": "^5.3.0", 87 | "shelljs": "^0.7.4", 88 | "sinon": "^1.17.3", 89 | "sinon-chai": "^2.8.0", 90 | "url-loader": "^0.5.7", 91 | "vue-loader": "^10.0.0", 92 | "vue-style-loader": "^1.0.0", 93 | "vue-template-compiler": "^2.1.0", 94 | "webpack": "^1.13.2", 95 | "webpack-dev-middleware": "^1.8.3", 96 | "webpack-hot-middleware": "^2.12.2", 97 | "webpack-merge": "^0.14.1" 98 | }, 99 | "engines": { 100 | "node": ">= 4.0.0", 101 | "npm": ">= 3.0.0" 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /server/.env: -------------------------------------------------------------------------------- 1 | JWT_SECRET = salt 2 | -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | node_modules 3 | coverage -------------------------------------------------------------------------------- /server/api/index.js: -------------------------------------------------------------------------------- 1 | var User = require('../lib/mongo').User; 2 | var Classify = require('../lib/mongo').Classify; 3 | var Article = require('../lib/mongo').Article; 4 | module.exports = { 5 | create:function (user){ 6 | return User.create(user).exec(); 7 | }, 8 | getUserByName:function (name){ 9 | return User.findOne({name:name}).exec(); 10 | }, 11 | // 创建分类 12 | createClass:function (data){ 13 | return Classify.create(data).exec(); 14 | }, 15 | // 删除分类 16 | removeClass:function (classId){ 17 | return Classify.remove({_id:classId}).exec(); 18 | }, 19 | // 编辑分类 20 | updateClass:function (classId,data){ 21 | return Classify.update({_id:classId},{$set:data}).exec() 22 | }, 23 | // 查询所有分类 24 | findAllClass:function (){ 25 | return Classify.find() 26 | .addCreateAt() 27 | .sort({_id:-1}) 28 | .exec(); 29 | }, 30 | // 创建文章 31 | createArticle:function (params){ 32 | return Article.create(params).exec(); 33 | }, 34 | // 获取所有文章 35 | getAllArticles:function (page,limit){ 36 | 37 | if(page&&limit){ 38 | var skip = (page-1)*limit 39 | return Promise.all([ 40 | Article.find().addCreateAt().sort({_id:-1}).skip(skip).limit(limit).exec(), 41 | Article.count().exec() 42 | ]) 43 | }else{ 44 | return Article.find() 45 | .addCreateAt() 46 | .sort({_id:-1}) 47 | .exec(); 48 | } 49 | 50 | }, 51 | // 根据classify获取所有文章 52 | getArticlesByClassify:function (classify){ 53 | return Article.find({classify}) 54 | .addCreateAt() 55 | .sort({_id:-1}) 56 | .exec(); 57 | }, 58 | getOneArticle(postId){ 59 | return Article.findOne({_id:postId}) 60 | .addCreateAt() 61 | .exec(); 62 | }, 63 | // 删除一篇文章 64 | removeOneArticle:function (postId){ 65 | return Article.remove({_id:postId}).exec(); 66 | }, 67 | // 编辑一篇文章 68 | updateArticle:function (postId,data){ 69 | return Article.update({_id:postId},{$set:data}).exec() 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var path = require('path'); 3 | var app = express(); 4 | var bodyParser = require('body-parser'); 5 | var routes = require('./routes'); 6 | app.use(bodyParser.urlencoded({ extended: true })); 7 | app.use(bodyParser.json()); 8 | // 跨域 9 | app.all('*', function(req, res, next) { 10 | res.header("Access-Control-Allow-Origin", "*"); 11 | res.header("Access-Control-Allow-Headers", "X-Requested-With,Content-Type");//预检请求使用 12 | res.header("Access-Control-Allow-Methods","PUT,POST,GET,DELETE,OPTIONS");//预检请求使用 13 | next(); 14 | }); 15 | routes(app); 16 | app.set('port', process.env.PORT || 3009); 17 | app.listen(app.get('port'), function() { 18 | console.log('Express server listening on port ' + app.get('port')); 19 | }); 20 | 21 | -------------------------------------------------------------------------------- /server/lib/mongo.js: -------------------------------------------------------------------------------- 1 | var Mongolass = require('mongolass'); 2 | var mongolass = new Mongolass(); 3 | mongolass.connect('mongodb://localhost:27017/mywebsite'); 4 | var moment = require('moment');//时间格式化(前后台都可以用的npm包) 5 | var objectIdToTimestamp = require('objectid-to-timestamp');// 根据_id生成时间戳 6 | /* 7 | mongolass插件系统,语法: 8 | mongolass.plugin(插件名字,{ 9 | before(方法) 10 | after(方法) 11 | }) 12 | */ 13 | mongolass.plugin('addCreateAt',{ 14 | // 只要查询所有条件,那么一定会有最终结果 15 | afterFind:function (results){ 16 | results.forEach(function (item){ 17 | item.created_at =item.created_at = moment(objectIdToTimestamp(item._id)).format('YYYY-MM-DD HH:mm:ss'); 18 | }) 19 | return results 20 | }, 21 | // 单个查询有可能是null,所以要加if 22 | afterFindOne:function (result){ 23 | if(result){ 24 | result.created_at =result.created_at = moment(objectIdToTimestamp(result._id)).format('YYYY-MM-DD HH:mm:ss'); 25 | } 26 | return result 27 | } 28 | }) 29 | // 用户 30 | exports.User = mongolass.model('User',{ 31 | name:{type:'string'}, 32 | password:{type:'string'} 33 | }) 34 | exports.User.index({name:1},{unique:true}).exec(); 35 | 36 | // 分类 37 | exports.Classify = mongolass.model('Classify',{ 38 | classify:{type:'string'} 39 | }) 40 | exports.Classify.index({_id:1}).exec(); 41 | 42 | // 文章 43 | exports.Article = mongolass.model('Article',{ 44 | classify:{type:"string"}, 45 | title:{type:'string'}, 46 | content:{type:'string'}, 47 | contentToMark:{type:'string'} 48 | }) 49 | exports.Article.index({_id:1,classify:-1}).exec(); 50 | -------------------------------------------------------------------------------- /server/middleware/checkToken.js: -------------------------------------------------------------------------------- 1 | // 验证token中间件 2 | var jwt = require('jsonwebtoken') 3 | module.exports = function (req, res, next) { 4 | if(req.headers['authorization']){ 5 | var token = req.headers['authorization'].split(' ')[1] 6 | var decoded = jwt.decode(token, process.env.JWT_SECRET) 7 | // 如果过期了就重新登录 8 | // 验证token也需要优化 9 | if(token&&decoded.exp<=Date.now()/1000){ 10 | return res.send({ 11 | code:401, 12 | message:"授权已经过期,请重新登陆" 13 | }) 14 | } 15 | } 16 | 17 | next(); 18 | } 19 | -------------------------------------------------------------------------------- /server/middleware/createToken.js: -------------------------------------------------------------------------------- 1 | var jwt = require('jsonwebtoken') 2 | module.exports = function (name){ 3 | var expiry = new Date(); 4 | expiry.setDate(expiry.getDate()+7);//有效期设置为七天 5 | const token = jwt.sign({ 6 | name:name, 7 | exp:parseInt(expiry.getTime()/1000)//除以1000以后表示的是秒数 8 | },process.env.JWT_SECRET) 9 | return token; 10 | } 11 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "cross-env NODE_ENV=production pm2 start app.js --node-args='--harmony' --name 'server'" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "body-parser": "^1.17.1", 14 | "config-lite": "^2.0.0", 15 | "connect-flash": "^0.1.1", 16 | "connect-mongo": "^1.3.2", 17 | "dot-env": "0.0.1", 18 | "dotenv": "^4.0.0", 19 | "ejs": "^2.5.6", 20 | "express": "^4.15.2", 21 | "express-formidable": "^1.0.0", 22 | "express-session": "^1.15.2", 23 | "express-winston": "^2.3.0", 24 | "jsonwebtoken": "^7.3.0", 25 | "marked": "^0.3.6", 26 | "moment": "^2.18.1", 27 | "mongolass": "^2.4.5", 28 | "objectid-to-timestamp": "^1.3.0", 29 | "sha1": "^1.1.1", 30 | "winston": "^2.3.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /server/routes/admin.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var checkToekn = require('../middleware/checkToken'); 3 | var router = express.Router(); 4 | router.get('/admin',checkToekn,function (req,res,next){ 5 | res.send({ 6 | type:true, 7 | name:'dailu' 8 | }); 9 | }); 10 | module.exports = router 11 | -------------------------------------------------------------------------------- /server/routes/article.js: -------------------------------------------------------------------------------- 1 | var express =require('express'); 2 | var router = express.Router(); 3 | var api = require('../api'); 4 | var checkToken = require('../middleware/checkToken'); 5 | // 创建一篇文章 6 | router.post('/article/create',checkToken,function (req,res,next){ 7 | api.createArticle(req.body) 8 | .then(({result:{ok,n}})=>{ 9 | if(ok&&n>0){ 10 | res.send({ 11 | code:200, 12 | message:'发布成功' 13 | }) 14 | }else{ 15 | throw new Error("发布失败"); 16 | } 17 | }) 18 | .catch(err=>{ 19 | res.send({ 20 | code:-200, 21 | message:err.toString() 22 | }) 23 | }) 24 | }) 25 | // 获取所有文章(带分页获取,需要验证权限) 26 | router.post('/article/lists',checkToken,function (req,res,next){ 27 | let {page,limit} =req.body 28 | api.getAllArticles(page,limit) 29 | .then((result)=>{ 30 | var articleLists = result[0], 31 | total = result[1]; 32 | articleLists.forEach((article=>{ 33 | // delete article._id 34 | delete article.content 35 | delete article.contentToMark 36 | })) 37 | res.send({ 38 | code:200, 39 | articleLists, 40 | total 41 | }) 42 | }) 43 | .catch(err=>{ 44 | res.send({ 45 | code:-200, 46 | message:err.toString() 47 | }) 48 | }) 49 | }) 50 | // 根据classify获取文章列表(前台使用没有权限) 51 | router.post('/article/noAuthArtilcelists',function (req,res,next){ 52 | 53 | let {classify} =req.body 54 | api.getArticlesByClassify(classify) 55 | .then((articleLists)=>{ 56 | articleLists.forEach((article=>{ 57 | delete article.content 58 | // delete article.title 59 | article.contentToMark = article.contentToMark.match(/

([\s\S]*?)<\/p>/g)[0] 60 | })) 61 | res.send({ 62 | code:200, 63 | articleLists 64 | }) 65 | }) 66 | .catch(err=>{ 67 | res.send({ 68 | code:-200, 69 | message:err.toString() 70 | }) 71 | }) 72 | }) 73 | // 获取所有文章(每次返回10个)前台使用 74 | router.post('/article/articleLists',function (req,res,next){ 75 | let {page,limit} =req.body 76 | api.getAllArticles(page,limit) 77 | .then((result)=>{ 78 | var articleLists = result[0], 79 | total = result[1], 80 | totalPage =Math.ceil(total/limit), 81 | hasNext=totalPage>page?1:0, 82 | hasPrev=page>1 83 | articleLists.forEach((article=>{ 84 | delete article.content 85 | article.contentToMark = article.contentToMark.match(/

([\s\S]*?)<\/p>/g)[0] 86 | })) 87 | 88 | res.send({ 89 | code:200, 90 | articleLists, 91 | hasNext, 92 | hasPrev 93 | }) 94 | }) 95 | .catch(err=>{ 96 | res.send({ 97 | code:-200, 98 | message:err.toString() 99 | }) 100 | }) 101 | }) 102 | // 根据postId获取其中一篇文章(有权限) 103 | router.post('/article/onePage',checkToken,function (req,res,next){ 104 | let {id} =req.body 105 | api.getOneArticle(id) 106 | .then((oneArticle)=>{ 107 | if(oneArticle){ 108 | res.send({ 109 | code:200, 110 | oneArticle 111 | }) 112 | }else{ 113 | throw new Error('没有找到该文章'); 114 | } 115 | }) 116 | .catch(err=>{ 117 | res.send({ 118 | code:-200, 119 | message:err.toString() 120 | }) 121 | }) 122 | }), 123 | // 根据postId获取其中一篇文章(没有权限) 124 | router.post('/article/noAuth',function (req,res,next){ 125 | let {id} =req.body 126 | api.getOneArticle(id) 127 | .then((oneArticle)=>{ 128 | if(oneArticle){ 129 | res.send({ 130 | code:200, 131 | oneArticle 132 | }) 133 | }else{ 134 | throw new Error('没有找到该文章'); 135 | } 136 | }) 137 | .catch(err=>{ 138 | res.send({ 139 | code:-200, 140 | message:err.toString() 141 | }) 142 | }) 143 | }), 144 | // 删除一篇文章 145 | router.post('/article/remove',checkToken,function (req,res,next){ 146 | api.removeOneArticle(req.body.id) 147 | .then(({result:{ok,n}})=>{ 148 | if(ok&&n>0){ 149 | res.send({ 150 | code:200, 151 | message:'删除成功' 152 | }) 153 | }else{ 154 | throw new Error('该文章不存在'); 155 | } 156 | }) 157 | .catch(err=>{ 158 | res.send({ 159 | code:-200, 160 | message:err.toString() 161 | }) 162 | }) 163 | }), 164 | // 编辑文章 165 | router.post('/article/edit',checkToken,function (req,res,next){ 166 | console.log(req.body); 167 | var id = req.body.id; 168 | var classify = req.body.classify 169 | var title = req.body.title 170 | var content = req.body.content 171 | var contentToMark = req.body.contentToMark 172 | api.updateArticle(id,{classify,title,content,contentToMark}) 173 | .then(({result:{ok,n}})=>{ 174 | if(ok&&n>0){ 175 | res.send({ 176 | code:200, 177 | message:'编辑成功' 178 | }) 179 | }else { 180 | throw new Error('编辑失败'); 181 | } 182 | }) 183 | .catch(err=>{ 184 | res.send({ 185 | code:-200, 186 | message:err.toString() 187 | }) 188 | }) 189 | }) 190 | module.exports = router 191 | -------------------------------------------------------------------------------- /server/routes/classify.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | var api = require('../api'); 4 | var checkToken = require('../middleware/checkToken'); 5 | // 创建分类 6 | router.post('/classify/create',checkToken,function (req,res,next){ 7 | api.createClass(req.body) 8 | .then(({result:{ok,n}})=>{ 9 | if(ok&&n>1){ 10 | res.send({ 11 | code:200, 12 | message:'创建成功' 13 | }) 14 | }else{ 15 | throw new Error('创建失败'); 16 | } 17 | }) 18 | .catch(err=>{ 19 | res.send({ 20 | code:-200, 21 | message:err.toString() 22 | }) 23 | }) 24 | }) 25 | // 删除分类 26 | router.post('/classify/remove',checkToken,function (req,res,next){ 27 | api.removeClass(req.body.id) 28 | .then(({result:{ok,n}})=>{ 29 | // 使用es6解构 30 | if(ok&&n>0){ 31 | // 已经删除了数据库中存在的项 32 | res.send({ 33 | code:200, 34 | message:'删除成功' 35 | }) 36 | }else{ 37 | // 删除不存在的项 38 | throw new Error('该分类不存在') 39 | } 40 | }) 41 | .catch(err=>{ 42 | res.send({ 43 | code:-200, 44 | message:err.toString() 45 | }) 46 | }) 47 | }) 48 | 49 | // 编辑分类 50 | router.post('/classify/edit',checkToken,function (req,res,next){ 51 | 52 | var id = req.body.id; 53 | var classify = req.body.classify 54 | api.updateClass(id,{classify:classify}) 55 | .then(({result:{ok,n}})=>{ 56 | if(ok&&n>0){ 57 | res.send({ 58 | code:200, 59 | message:'编辑成功' 60 | }) 61 | }else{ 62 | throw new Error('编辑失败'); 63 | } 64 | }) 65 | .catch(err=>{ 66 | res.send({ 67 | code:-200, 68 | message:err.toString() 69 | }) 70 | }) 71 | }) 72 | 73 | // 获取所有分类 74 | router.get('/classify/lists',checkToken,function (req,res,next){ 75 | api.findAllClass() 76 | .then((lists)=>{ 77 | res.send({ 78 | code:200, 79 | lists 80 | }) 81 | }) 82 | .catch(err=>{ 83 | res.send({ 84 | code:-200, 85 | message:err.toString() 86 | }) 87 | }) 88 | }) 89 | // 无权限获取分类给前台使用 90 | router.get('/classify/noAuth',function (req,res,next){ 91 | api.findAllClass() 92 | .then((lists)=>{ 93 | res.send({ 94 | code:200, 95 | lists 96 | }) 97 | }) 98 | .catch(err=>{ 99 | res.send({ 100 | code:-200, 101 | message:err.toString() 102 | }) 103 | }) 104 | }) 105 | 106 | module.exports = router; 107 | -------------------------------------------------------------------------------- /server/routes/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function (app){ 2 | app.use('/api',require('./reg')); 3 | app.use('/api',require('./login')); 4 | app.use('/api',require('./admin')); 5 | app.use('/api',require('./classify')); 6 | app.use('/api',require('./article')); 7 | } 8 | -------------------------------------------------------------------------------- /server/routes/login.js: -------------------------------------------------------------------------------- 1 | require('dotenv').load(); 2 | var express = require('express'); 3 | var router = express.Router(); 4 | var api = require('../api'); 5 | var createToken = require('../middleware/createToken'); 6 | var sha1 = require('sha1'); 7 | router.post('/login',function (req,res,next){ 8 | var name= req.body.account; 9 | var password = sha1(req.body.checkPass); 10 | api.getUserByName(req.body.account) 11 | .then(function(user){ 12 | if(user&&(password==user.password)){ 13 | // 用户名存在通过验证 14 | res.json({ 15 | code:200, 16 | token:createToken(name) 17 | }); 18 | }else{ 19 | // 用户名或者密码错误没有通过验证,要么重新输入,要么点击注册() 20 | res.json({ 21 | code:-200, 22 | message:'用户名或密码错误' 23 | }) 24 | } 25 | }) 26 | .catch(err=>{ 27 | // 查找数据库发生错误,或者一些 28 | next(err) 29 | return res.json({ 30 | code:-200, 31 | message:err.toString() 32 | }) 33 | }) 34 | }) 35 | module.exports = router; 36 | -------------------------------------------------------------------------------- /server/routes/reg.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | var UserModel = require('../api'); 3 | var express = require('express'); 4 | var router = express.Router(); 5 | var sha1 = require('sha1'); 6 | var createToken = require('../middleware/createToken'); 7 | router.post('/reg',function (req,res,next) { 8 | var name = req.body.account; 9 | var password = req.body.checkPass; 10 | password = sha1(password); 11 | var user = { 12 | name:name, 13 | password:password 14 | } 15 | 16 | UserModel.create(user) 17 | .then(()=>{ 18 | res.send({ 19 | // 创建用户成功 20 | code:200, 21 | token:createToken(name) 22 | }) 23 | }) 24 | .catch(err=>{ 25 | // 操作数据库的时候发生错误 26 | if(err.message.match('E11000 duplicate key')){ 27 | return res.json({ 28 | code:-200, 29 | message:'用户名重复' 30 | }) 31 | } 32 | // 服务器发生错误(例如status:) 33 | return res.json({ 34 | code:-200, 35 | message:err.toString() 36 | }) 37 | }) 38 | }) 39 | module.exports = router; 40 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 36 | 37 | 43 | -------------------------------------------------------------------------------- /src/api/index.js: -------------------------------------------------------------------------------- 1 | // 各种api 2 | // 负责用instance和服务端进行交互 3 | 4 | import axios from 'axios'; 5 | import store from '../store' 6 | // axios.defaults.headers.common['Authorization'] = 'dailu'; 7 | axios.defaults.headers.post['Content-Type'] = 'application/json' 8 | 9 | const instance = axios.create(); 10 | const front_instance = axios.create(); 11 | instance.defaults.headers.post['Content-Type'] = 'application/json' 12 | if(localStorage.getItem('jwt')){ 13 | /* localStorage.getItem('jwt')是带引号的字符串 14 | Bearer token(通过Authorization头部字段发送到服务端便于验证)的格式:Bearer XXXXXXXXXX 15 | */ 16 | instance.defaults.headers.common['Authorization'] = "Bearer "+localStorage.getItem('jwt').replace(/(^\")|(\"$)/g,'') 17 | } 18 | // axios拦截请求 19 | axios.interceptors.request.use = instance.interceptors.request.use = front_instance.interceptors.request.use 20 | front_instance.interceptors.request.use(config=>{ 21 | store.dispatch('showProgress',20) 22 | return config 23 | },err=>{ 24 | // store.dispatch('showProgress',100) 25 | return Promise.reject(err) 26 | }) 27 | // axios拦截响应 28 | front_instance.interceptors.response.use(response=>{ 29 | store.dispatch('showProgress',100) 30 | return response 31 | },err=>{ 32 | store.dispatch('showProgress',100) 33 | return Promise.reject(err) 34 | }) 35 | export default { 36 | // 注册 37 | localReg(data){ 38 | return axios.post('/api/reg',data) 39 | }, 40 | // 登录 41 | localLogin(data){ 42 | return axios.post('/api/login',data) 43 | }, 44 | //获取文章列表{带分页获取} 45 | getArticleList(data){ 46 | return instance.post('/api/article/lists',data); 47 | }, 48 | // 不带分页获取文章 49 | getArticleLists(params){ 50 | return front_instance.post('/api/article/articleLists',params); 51 | }, 52 | // 根据classify获取文章列表 53 | getArticlesByClassify(params){ 54 | return front_instance.post('/api/article/noAuthArtilcelists',params); 55 | }, 56 | // 创建文章 57 | createArticle(params){ 58 | return instance.post('/api/article/create',params); 59 | }, 60 | // 删除一篇文章 61 | removeOneArticle(params){ 62 | return instance.post('/api/article/remove',params); 63 | }, 64 | // 根据postID获取一篇文章(带权限) 65 | getOneArticle(params){ 66 | return instance.post('/api/article/onePage',params); 67 | }, 68 | // 根据postID获取一篇文章(不带权限) 69 | getOneArticleNoAuth(params){ 70 | return front_instance.post('/api/article/noAuth',params); 71 | }, 72 | // 编辑一篇文章 73 | editArticle(params){ 74 | return instance.post('/api/article/edit',params); 75 | }, 76 | // 获取分类列表 77 | getClassify(){ 78 | return instance.get('/api/classify/lists'); 79 | }, 80 | getNoAuthClass(){ 81 | return front_instance.get('/api/classify/noAuth'); 82 | }, 83 | 84 | // 删除某一个分类 85 | removeClassifyList(params){ 86 | return instance.post('/api/classify/remove',params); 87 | }, 88 | // 添加分类 89 | addClassify(params){ 90 | return instance.post('/api/classify/create',params); 91 | }, 92 | 93 | // 编辑分类 94 | editClassfy(params){ 95 | return instance.post('/api/classify/edit',params); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/assets/css/commen.css: -------------------------------------------------------------------------------- 1 | /*通用样式*/ 2 | *{ 3 | box-sizing: border-box; 4 | } 5 | html{ 6 | font-size: 10px; 7 | height: 100%; 8 | /*background:#ebf0f0;*/ 9 | /*百分百撑起高度*/ 10 | } 11 | body{ 12 | margin:0; 13 | -webkit-font-smoothing:antialiased; 14 | font-size: 1.6rem; 15 | margin: 0; 16 | padding: 0; 17 | font-family: -apple-system, "Helvetica Neue", Arial, "PingFang SC", "Hiragino Sans GB", STHeiti, "Microsoft YaHei", "Microsoft JhengHei", "Source Han Sans SC", "Noto Sans CJK SC", "Source Han Sans CN", "Noto Sans SC", "Source Han Sans TC", "Noto Sans CJK TC", "WenQuanYi Micro Hei", SimSun, sans-serif; 18 | color:#404040; 19 | height: 100%; 20 | overflow-y: scroll; 21 | } 22 | /*样式重置*/ 23 | a{ 24 | text-decoration: none; 25 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 26 | } 27 | /*登录注册通用样式*/ 28 | .container{ 29 | margin:0; 30 | background:url('../img/bg.jpg') no-repeat center; 31 | background-size: cover; 32 | min-width: 680px; 33 | position: absolute; 34 | top:0; 35 | bottom:0; 36 | left:0; 37 | right:0; 38 | } 39 | .title{ 40 | text-align: center; 41 | margin: 0px auto 40px auto; 42 | text-align: center; 43 | color: #505458; 44 | } 45 | .login_form,.reg_form{ 46 | -webkit-border-radius: 5px; 47 | border-radius: 5px; 48 | -moz-border-radius: 5px; 49 | background-color: #F9FAFC; 50 | margin: 150px auto 20px auto ; 51 | border: 2px solid #8492A6; 52 | width: 350px; 53 | padding: 35px 35px 15px 35px; 54 | opacity: 0.6; 55 | } 56 | .reg_button,.login_button{ 57 | margin-top: 20px; 58 | } 59 | pre{ 60 | /*white-space: pre-wrap;*/ 61 | } 62 | code{ 63 | color:#657b83; 64 | background: #f6f6f6; 65 | font: 16px Consolas, "Liberation Mono", Menlo, Courier, monospace; 66 | padding: .2rem .4rem; 67 | max-width: 100%; 68 | border-radius: 5px; 69 | } 70 | li code, 71 | p code{ 72 | word-wrap: break-word; 73 | /* line-height: */ 74 | border-radius: 5px; 75 | font-size: 14px; 76 | } 77 | .article pre code{ 78 | color:#8492A6; 79 | display: block; 80 | background: #23241f; 81 | font: 16px Consolas, "Liberation Mono", Menlo, Courier, monospace; 82 | border-radius: 5px; 83 | max-width: 100%; 84 | line-height: 1.5em; 85 | padding: .4rem .6rem; 86 | overflow-x: auto; 87 | } 88 | .article img,.showArtilce img{ 89 | max-width: 100%; 90 | display: block; 91 | } 92 | .article a{ 93 | color: #3194d0; 94 | } 95 | .article a:hover { 96 | text-decoration: underline; 97 | } 98 | .article ul { 99 | padding-left: 2.2rem; 100 | } 101 | .article h1{ 102 | font-size: 26px; 103 | } 104 | .article h2{ 105 | font-size: 24px; 106 | } 107 | .article h3{ 108 | font-size: 22px; 109 | } 110 | .article h4{ 111 | font-size: 20px; 112 | } 113 | .article h5{ 114 | font-size: 18px; 115 | } 116 | .article h6{ 117 | font-size:16px; 118 | } 119 | .article p{ 120 | margin-bottom: 2.5rem; 121 | } 122 | .tags_main p{ 123 | margin:0; 124 | } 125 | h1, 126 | h2, 127 | h3, 128 | h4, 129 | h5, 130 | h6{ 131 | line-height: 1.4em; 132 | } 133 | @media screen and (max-width:768px){ 134 | .article pre code{ 135 | font-size: 14px; 136 | } 137 | .article h1{ 138 | font-size: 24px; 139 | } 140 | .article h2{ 141 | font-size: 22px; 142 | } 143 | .article h3{ 144 | font-size: 20px; 145 | } 146 | .article h4{ 147 | font-size: 18px; 148 | } 149 | .article h5{ 150 | font-size: 16px; 151 | } 152 | .article h6{ 153 | font-size:14px; 154 | } 155 | p code{ 156 | font-size: 12px; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/assets/css/default.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Original highlight.js style (c) Ivan Sagalaev 4 | 5 | */ 6 | 7 | .hljs { 8 | display: block; 9 | overflow-x: auto; 10 | padding: 0.5em; 11 | background: #F0F0F0; 12 | } 13 | 14 | 15 | /* Base color: saturation 0; */ 16 | 17 | .hljs, 18 | .hljs-subst { 19 | color: #444; 20 | } 21 | 22 | .hljs-comment { 23 | color: #888888; 24 | } 25 | 26 | .hljs-keyword, 27 | .hljs-attribute, 28 | .hljs-selector-tag, 29 | .hljs-meta-keyword, 30 | .hljs-doctag, 31 | .hljs-name { 32 | font-weight: bold; 33 | } 34 | 35 | 36 | /* User color: hue: 0 */ 37 | 38 | .hljs-type, 39 | .hljs-string, 40 | .hljs-number, 41 | .hljs-selector-id, 42 | .hljs-selector-class, 43 | .hljs-quote, 44 | .hljs-template-tag, 45 | .hljs-deletion { 46 | color: #880000; 47 | } 48 | 49 | .hljs-title, 50 | .hljs-section { 51 | color: #880000; 52 | font-weight: bold; 53 | } 54 | 55 | .hljs-regexp, 56 | .hljs-symbol, 57 | .hljs-variable, 58 | .hljs-template-variable, 59 | .hljs-link, 60 | .hljs-selector-attr, 61 | .hljs-selector-pseudo { 62 | color: #BC6060; 63 | } 64 | 65 | 66 | /* Language color: hue: 90; */ 67 | 68 | .hljs-literal { 69 | color: #78A960; 70 | } 71 | 72 | .hljs-built_in, 73 | .hljs-bullet, 74 | .hljs-code, 75 | .hljs-addition { 76 | color: #397300; 77 | } 78 | 79 | 80 | /* Meta color: hue: 200 */ 81 | 82 | .hljs-meta { 83 | color: #1f7199; 84 | } 85 | 86 | .hljs-meta-string { 87 | color: #4d99bf; 88 | } 89 | 90 | 91 | /* Misc effects */ 92 | 93 | .hljs-emphasis { 94 | font-style: italic; 95 | } 96 | 97 | .hljs-strong { 98 | font-weight: bold; 99 | } 100 | -------------------------------------------------------------------------------- /src/assets/css/default0.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Original highlight.js style (c) Ivan Sagalaev 4 | 5 | */ 6 | 7 | .hljs { 8 | display: block; 9 | overflow-x: auto; 10 | padding: 0.5em; 11 | background: #F0F0F0; 12 | } 13 | 14 | 15 | /* Base color: saturation 0; */ 16 | 17 | .hljs, 18 | .hljs-subst { 19 | color: #444; 20 | } 21 | 22 | .hljs-comment { 23 | color: #888888; 24 | } 25 | 26 | .hljs-keyword, 27 | .hljs-attribute, 28 | .hljs-selector-tag, 29 | .hljs-meta-keyword, 30 | .hljs-doctag, 31 | .hljs-name { 32 | font-weight: bold; 33 | } 34 | 35 | 36 | /* User color: hue: 0 */ 37 | 38 | .hljs-type, 39 | .hljs-string, 40 | .hljs-number, 41 | .hljs-selector-id, 42 | .hljs-selector-class, 43 | .hljs-quote, 44 | .hljs-template-tag, 45 | .hljs-deletion { 46 | color: #880000; 47 | } 48 | 49 | .hljs-title, 50 | .hljs-section { 51 | color: #880000; 52 | font-weight: bold; 53 | } 54 | 55 | .hljs-regexp, 56 | .hljs-symbol, 57 | .hljs-variable, 58 | .hljs-template-variable, 59 | .hljs-link, 60 | .hljs-selector-attr, 61 | .hljs-selector-pseudo { 62 | color: #BC6060; 63 | } 64 | 65 | 66 | /* Language color: hue: 90; */ 67 | 68 | .hljs-literal { 69 | color: #78A960; 70 | } 71 | 72 | .hljs-built_in, 73 | .hljs-bullet, 74 | .hljs-code, 75 | .hljs-addition { 76 | color: #397300; 77 | } 78 | 79 | 80 | /* Meta color: hue: 200 */ 81 | 82 | .hljs-meta { 83 | color: #1f7199; 84 | } 85 | 86 | .hljs-meta-string { 87 | color: #4d99bf; 88 | } 89 | 90 | 91 | /* Misc effects */ 92 | 93 | .hljs-emphasis { 94 | font-style: italic; 95 | } 96 | 97 | .hljs-strong { 98 | font-weight: bold; 99 | } 100 | -------------------------------------------------------------------------------- /src/assets/css/highlight.css: -------------------------------------------------------------------------------- 1 | .hljs-comment, 2 | .hljs-quote { 3 | color: #8e908c; 4 | } 5 | 6 | .hljs-variable, 7 | .hljs-template-variable, 8 | .hljs-tag, 9 | .hljs-name, 10 | .hljs-selector-id, 11 | .hljs-selector-class, 12 | .hljs-regexp, 13 | .hljs-deletion { 14 | color: #c82829; 15 | } 16 | 17 | .hljs-number, 18 | .hljs-built_in, 19 | .hljs-builtin-name, 20 | .hljs-literal, 21 | .hljs-type, 22 | .hljs-params, 23 | .hljs-meta, 24 | .hljs-link { 25 | color: #f5871f; 26 | } 27 | 28 | .hljs-attribute { 29 | color: #eab700; 30 | } 31 | 32 | .hljs-string, 33 | .hljs-symbol, 34 | .hljs-bullet, 35 | .hljs-addition { 36 | color: #718c00; 37 | } 38 | 39 | .hljs-title, 40 | .hljs-section { 41 | color: #4271ae; 42 | } 43 | 44 | .hljs-keyword, 45 | .hljs-selector-tag { 46 | color: #8959a8; 47 | } 48 | 49 | .hljs { 50 | display: block; 51 | overflow-x: auto; 52 | background: white; 53 | color: #4d4d4c; 54 | padding: 0.5em; 55 | } 56 | 57 | .hljs-emphasis { 58 | font-style: italic; 59 | } 60 | 61 | .hljs-strong { 62 | font-weight: bold; 63 | } -------------------------------------------------------------------------------- /src/assets/img/backend.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elva2596/vueBlog/e172b019024b1ef15f87dcaf5db40cc10e7f9df2/src/assets/img/backend.gif -------------------------------------------------------------------------------- /src/assets/img/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elva2596/vueBlog/e172b019024b1ef15f87dcaf5db40cc10e7f9df2/src/assets/img/bg.jpg -------------------------------------------------------------------------------- /src/assets/img/bgm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elva2596/vueBlog/e172b019024b1ef15f87dcaf5db40cc10e7f9df2/src/assets/img/bgm.jpg -------------------------------------------------------------------------------- /src/assets/img/fronted.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elva2596/vueBlog/e172b019024b1ef15f87dcaf5db40cc10e7f9df2/src/assets/img/fronted.gif -------------------------------------------------------------------------------- /src/assets/img/mobile.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elva2596/vueBlog/e172b019024b1ef15f87dcaf5db40cc10e7f9df2/src/assets/img/mobile.gif -------------------------------------------------------------------------------- /src/assets/js/commen.js: -------------------------------------------------------------------------------- 1 | export function sub(obj,res){ 2 | obj.btnText="提交"; 3 | obj.editLoading = false; 4 | 5 | if(res.data.code==200){ 6 | obj.$notify({ 7 | title:'成功', 8 | message:'提交成功', 9 | type:'success' 10 | }) 11 | }else if(res.data.code==401){ 12 | this.$notify({ 13 | title:'失败', 14 | message, 15 | type:'error' 16 | }) 17 | setTimeout(()=>{ 18 | this.$router.replace({path:'/login'}) 19 | },500) 20 | return false//阻止继续执行 21 | // 需要优化 22 | } 23 | obj.formVisible = false; 24 | obj.getLists(); 25 | } 26 | -------------------------------------------------------------------------------- /src/assets/js/highlight.pack.js: -------------------------------------------------------------------------------- 1 | /*! highlight.js v9.7.0 | BSD3 License | git.io/hljslicense */ 2 | (function(factory) { 3 | 4 | // Find the global object for export to both the browser and web workers. 5 | var globalObject = typeof window === 'object' && window || 6 | typeof self === 'object' && self; 7 | 8 | // Setup highlight.js for different environments. First is Node.js or 9 | // CommonJS. 10 | if(typeof exports !== 'undefined') { 11 | factory(exports); 12 | } else if(globalObject) { 13 | // Export hljs globally even when using AMD for cases when this script 14 | // is loaded with others that may still expect a global hljs. 15 | globalObject.hljs = factory({}); 16 | 17 | // Finally register the global hljs with AMD. 18 | if(typeof define === 'function' && define.amd) { 19 | define([], function() { 20 | return globalObject.hljs; 21 | }); 22 | } 23 | } 24 | 25 | }(function(hljs) { 26 | // Convenience variables for build-in objects 27 | var ArrayProto = [], 28 | objectKeys = Object.keys; 29 | 30 | // Global internal variables used within the highlight.js library. 31 | var languages = {}, 32 | aliases = {}; 33 | 34 | // Regular expressions used throughout the highlight.js library. 35 | var noHighlightRe = /^(no-?highlight|plain|text)$/i, 36 | languagePrefixRe = /\blang(?:uage)?-([\w-]+)\b/i, 37 | fixMarkupRe = /((^(<[^>]+>|\t|)+|(?:\n)))/gm; 38 | 39 | var spanEndTag = ''; 40 | 41 | // Global options used when within external APIs. This is modified when 42 | // calling the `hljs.configure` function. 43 | var options = { 44 | classPrefix: 'hljs-', 45 | tabReplace: null, 46 | useBR: false, 47 | languages: undefined 48 | }; 49 | 50 | // Object map that is used to escape some common HTML characters. 51 | var escapeRegexMap = { 52 | '&': '&', 53 | '<': '<', 54 | '>': '>' 55 | }; 56 | 57 | /* Utility functions */ 58 | 59 | function escape(value) { 60 | return value.replace(/[&<>]/gm, function(character) { 61 | return escapeRegexMap[character]; 62 | }); 63 | } 64 | 65 | function tag(node) { 66 | return node.nodeName.toLowerCase(); 67 | } 68 | 69 | function testRe(re, lexeme) { 70 | var match = re && re.exec(lexeme); 71 | return match && match.index === 0; 72 | } 73 | 74 | function isNotHighlighted(language) { 75 | return noHighlightRe.test(language); 76 | } 77 | 78 | function blockLanguage(block) { 79 | var i, match, length, _class; 80 | var classes = block.className + ' '; 81 | 82 | classes += block.parentNode ? block.parentNode.className : ''; 83 | 84 | // language-* takes precedence over non-prefixed class names. 85 | match = languagePrefixRe.exec(classes); 86 | if (match) { 87 | return getLanguage(match[1]) ? match[1] : 'no-highlight'; 88 | } 89 | 90 | classes = classes.split(/\s+/); 91 | 92 | for (i = 0, length = classes.length; i < length; i++) { 93 | _class = classes[i] 94 | 95 | if (isNotHighlighted(_class) || getLanguage(_class)) { 96 | return _class; 97 | } 98 | } 99 | } 100 | 101 | function inherit(parent, obj) { 102 | var key; 103 | var result = {}; 104 | 105 | for (key in parent) 106 | result[key] = parent[key]; 107 | if (obj) 108 | for (key in obj) 109 | result[key] = obj[key]; 110 | return result; 111 | } 112 | 113 | /* Stream merging */ 114 | 115 | function nodeStream(node) { 116 | var result = []; 117 | (function _nodeStream(node, offset) { 118 | for (var child = node.firstChild; child; child = child.nextSibling) { 119 | if (child.nodeType === 3) 120 | offset += child.nodeValue.length; 121 | else if (child.nodeType === 1) { 122 | result.push({ 123 | event: 'start', 124 | offset: offset, 125 | node: child 126 | }); 127 | offset = _nodeStream(child, offset); 128 | // Prevent void elements from having an end tag that would actually 129 | // double them in the output. There are more void elements in HTML 130 | // but we list only those realistically expected in code display. 131 | if (!tag(child).match(/br|hr|img|input/)) { 132 | result.push({ 133 | event: 'stop', 134 | offset: offset, 135 | node: child 136 | }); 137 | } 138 | } 139 | } 140 | return offset; 141 | })(node, 0); 142 | return result; 143 | } 144 | 145 | function mergeStreams(original, highlighted, value) { 146 | var processed = 0; 147 | var result = ''; 148 | var nodeStack = []; 149 | 150 | function selectStream() { 151 | if (!original.length || !highlighted.length) { 152 | return original.length ? original : highlighted; 153 | } 154 | if (original[0].offset !== highlighted[0].offset) { 155 | return (original[0].offset < highlighted[0].offset) ? original : highlighted; 156 | } 157 | 158 | /* 159 | To avoid starting the stream just before it should stop the order is 160 | ensured that original always starts first and closes last: 161 | 162 | if (event1 == 'start' && event2 == 'start') 163 | return original; 164 | if (event1 == 'start' && event2 == 'stop') 165 | return highlighted; 166 | if (event1 == 'stop' && event2 == 'start') 167 | return original; 168 | if (event1 == 'stop' && event2 == 'stop') 169 | return highlighted; 170 | 171 | ... which is collapsed to: 172 | */ 173 | return highlighted[0].event === 'start' ? original : highlighted; 174 | } 175 | 176 | function open(node) { 177 | function attr_str(a) {return ' ' + a.nodeName + '="' + escape(a.value) + '"';} 178 | result += '<' + tag(node) + ArrayProto.map.call(node.attributes, attr_str).join('') + '>'; 179 | } 180 | 181 | function close(node) { 182 | result += ''; 183 | } 184 | 185 | function render(event) { 186 | (event.event === 'start' ? open : close)(event.node); 187 | } 188 | 189 | while (original.length || highlighted.length) { 190 | var stream = selectStream(); 191 | result += escape(value.substr(processed, stream[0].offset - processed)); 192 | processed = stream[0].offset; 193 | if (stream === original) { 194 | /* 195 | On any opening or closing tag of the original markup we first close 196 | the entire highlighted node stack, then render the original tag along 197 | with all the following original tags at the same offset and then 198 | reopen all the tags on the highlighted stack. 199 | */ 200 | nodeStack.reverse().forEach(close); 201 | do { 202 | render(stream.splice(0, 1)[0]); 203 | stream = selectStream(); 204 | } while (stream === original && stream.length && stream[0].offset === processed); 205 | nodeStack.reverse().forEach(open); 206 | } else { 207 | if (stream[0].event === 'start') { 208 | nodeStack.push(stream[0].node); 209 | } else { 210 | nodeStack.pop(); 211 | } 212 | render(stream.splice(0, 1)[0]); 213 | } 214 | } 215 | return result + escape(value.substr(processed)); 216 | } 217 | 218 | /* Initialization */ 219 | 220 | function compileLanguage(language) { 221 | 222 | function reStr(re) { 223 | return (re && re.source) || re; 224 | } 225 | 226 | function langRe(value, global) { 227 | return new RegExp( 228 | reStr(value), 229 | 'm' + (language.case_insensitive ? 'i' : '') + (global ? 'g' : '') 230 | ); 231 | } 232 | 233 | function compileMode(mode, parent) { 234 | if (mode.compiled) 235 | return; 236 | mode.compiled = true; 237 | 238 | mode.keywords = mode.keywords || mode.beginKeywords; 239 | if (mode.keywords) { 240 | var compiled_keywords = {}; 241 | 242 | var flatten = function(className, str) { 243 | if (language.case_insensitive) { 244 | str = str.toLowerCase(); 245 | } 246 | str.split(' ').forEach(function(kw) { 247 | var pair = kw.split('|'); 248 | compiled_keywords[pair[0]] = [className, pair[1] ? Number(pair[1]) : 1]; 249 | }); 250 | }; 251 | 252 | if (typeof mode.keywords === 'string') { // string 253 | flatten('keyword', mode.keywords); 254 | } else { 255 | objectKeys(mode.keywords).forEach(function (className) { 256 | flatten(className, mode.keywords[className]); 257 | }); 258 | } 259 | mode.keywords = compiled_keywords; 260 | } 261 | mode.lexemesRe = langRe(mode.lexemes || /\w+/, true); 262 | 263 | if (parent) { 264 | if (mode.beginKeywords) { 265 | mode.begin = '\\b(' + mode.beginKeywords.split(' ').join('|') + ')\\b'; 266 | } 267 | if (!mode.begin) 268 | mode.begin = /\B|\b/; 269 | mode.beginRe = langRe(mode.begin); 270 | if (!mode.end && !mode.endsWithParent) 271 | mode.end = /\B|\b/; 272 | if (mode.end) 273 | mode.endRe = langRe(mode.end); 274 | mode.terminator_end = reStr(mode.end) || ''; 275 | if (mode.endsWithParent && parent.terminator_end) 276 | mode.terminator_end += (mode.end ? '|' : '') + parent.terminator_end; 277 | } 278 | if (mode.illegal) 279 | mode.illegalRe = langRe(mode.illegal); 280 | if (mode.relevance == null) 281 | mode.relevance = 1; 282 | if (!mode.contains) { 283 | mode.contains = []; 284 | } 285 | var expanded_contains = []; 286 | mode.contains.forEach(function(c) { 287 | if (c.variants) { 288 | c.variants.forEach(function(v) {expanded_contains.push(inherit(c, v));}); 289 | } else { 290 | expanded_contains.push(c === 'self' ? mode : c); 291 | } 292 | }); 293 | mode.contains = expanded_contains; 294 | mode.contains.forEach(function(c) {compileMode(c, mode);}); 295 | 296 | if (mode.starts) { 297 | compileMode(mode.starts, parent); 298 | } 299 | 300 | var terminators = 301 | mode.contains.map(function(c) { 302 | return c.beginKeywords ? '\\.?(' + c.begin + ')\\.?' : c.begin; 303 | }) 304 | .concat([mode.terminator_end, mode.illegal]) 305 | .map(reStr) 306 | .filter(Boolean); 307 | mode.terminators = terminators.length ? langRe(terminators.join('|'), true) : {exec: function(/*s*/) {return null;}}; 308 | } 309 | 310 | compileMode(language); 311 | } 312 | 313 | /* 314 | Core highlighting function. Accepts a language name, or an alias, and a 315 | string with the code to highlight. Returns an object with the following 316 | properties: 317 | 318 | - relevance (int) 319 | - value (an HTML string with highlighting markup) 320 | 321 | */ 322 | function highlight(name, value, ignore_illegals, continuation) { 323 | 324 | function subMode(lexeme, mode) { 325 | var i, length; 326 | 327 | for (i = 0, length = mode.contains.length; i < length; i++) { 328 | if (testRe(mode.contains[i].beginRe, lexeme)) { 329 | return mode.contains[i]; 330 | } 331 | } 332 | } 333 | 334 | function endOfMode(mode, lexeme) { 335 | if (testRe(mode.endRe, lexeme)) { 336 | while (mode.endsParent && mode.parent) { 337 | mode = mode.parent; 338 | } 339 | return mode; 340 | } 341 | if (mode.endsWithParent) { 342 | return endOfMode(mode.parent, lexeme); 343 | } 344 | } 345 | 346 | function isIllegal(lexeme, mode) { 347 | return !ignore_illegals && testRe(mode.illegalRe, lexeme); 348 | } 349 | 350 | function keywordMatch(mode, match) { 351 | var match_str = language.case_insensitive ? match[0].toLowerCase() : match[0]; 352 | return mode.keywords.hasOwnProperty(match_str) && mode.keywords[match_str]; 353 | } 354 | 355 | function buildSpan(classname, insideSpan, leaveOpen, noPrefix) { 356 | var classPrefix = noPrefix ? '' : options.classPrefix, 357 | openSpan = ''; 361 | 362 | return openSpan + insideSpan + closeSpan; 363 | } 364 | 365 | function processKeywords() { 366 | var keyword_match, last_index, match, result; 367 | 368 | if (!top.keywords) 369 | return escape(mode_buffer); 370 | 371 | result = ''; 372 | last_index = 0; 373 | top.lexemesRe.lastIndex = 0; 374 | match = top.lexemesRe.exec(mode_buffer); 375 | 376 | while (match) { 377 | result += escape(mode_buffer.substr(last_index, match.index - last_index)); 378 | keyword_match = keywordMatch(top, match); 379 | if (keyword_match) { 380 | relevance += keyword_match[1]; 381 | result += buildSpan(keyword_match[0], escape(match[0])); 382 | } else { 383 | result += escape(match[0]); 384 | } 385 | last_index = top.lexemesRe.lastIndex; 386 | match = top.lexemesRe.exec(mode_buffer); 387 | } 388 | return result + escape(mode_buffer.substr(last_index)); 389 | } 390 | 391 | function processSubLanguage() { 392 | var explicit = typeof top.subLanguage === 'string'; 393 | if (explicit && !languages[top.subLanguage]) { 394 | return escape(mode_buffer); 395 | } 396 | 397 | var result = explicit ? 398 | highlight(top.subLanguage, mode_buffer, true, continuations[top.subLanguage]) : 399 | highlightAuto(mode_buffer, top.subLanguage.length ? top.subLanguage : undefined); 400 | 401 | // Counting embedded language score towards the host language may be disabled 402 | // with zeroing the containing mode relevance. Usecase in point is Markdown that 403 | // allows XML everywhere and makes every XML snippet to have a much larger Markdown 404 | // score. 405 | if (top.relevance > 0) { 406 | relevance += result.relevance; 407 | } 408 | if (explicit) { 409 | continuations[top.subLanguage] = result.top; 410 | } 411 | return buildSpan(result.language, result.value, false, true); 412 | } 413 | 414 | function processBuffer() { 415 | result += (top.subLanguage != null ? processSubLanguage() : processKeywords()); 416 | mode_buffer = ''; 417 | } 418 | 419 | function startNewMode(mode) { 420 | result += mode.className? buildSpan(mode.className, '', true): ''; 421 | top = Object.create(mode, {parent: {value: top}}); 422 | } 423 | 424 | function processLexeme(buffer, lexeme) { 425 | 426 | mode_buffer += buffer; 427 | 428 | if (lexeme == null) { 429 | processBuffer(); 430 | return 0; 431 | } 432 | 433 | var new_mode = subMode(lexeme, top); 434 | if (new_mode) { 435 | if (new_mode.skip) { 436 | mode_buffer += lexeme; 437 | } else { 438 | if (new_mode.excludeBegin) { 439 | mode_buffer += lexeme; 440 | } 441 | processBuffer(); 442 | if (!new_mode.returnBegin && !new_mode.excludeBegin) { 443 | mode_buffer = lexeme; 444 | } 445 | } 446 | startNewMode(new_mode, lexeme); 447 | return new_mode.returnBegin ? 0 : lexeme.length; 448 | } 449 | 450 | var end_mode = endOfMode(top, lexeme); 451 | if (end_mode) { 452 | var origin = top; 453 | if (origin.skip) { 454 | mode_buffer += lexeme; 455 | } else { 456 | if (!(origin.returnEnd || origin.excludeEnd)) { 457 | mode_buffer += lexeme; 458 | } 459 | processBuffer(); 460 | if (origin.excludeEnd) { 461 | mode_buffer = lexeme; 462 | } 463 | } 464 | do { 465 | if (top.className) { 466 | result += spanEndTag; 467 | } 468 | if (!top.skip) { 469 | relevance += top.relevance; 470 | } 471 | top = top.parent; 472 | } while (top !== end_mode.parent); 473 | if (end_mode.starts) { 474 | startNewMode(end_mode.starts, ''); 475 | } 476 | return origin.returnEnd ? 0 : lexeme.length; 477 | } 478 | 479 | if (isIllegal(lexeme, top)) 480 | throw new Error('Illegal lexeme "' + lexeme + '" for mode "' + (top.className || '') + '"'); 481 | 482 | /* 483 | Parser should not reach this point as all types of lexemes should be caught 484 | earlier, but if it does due to some bug make sure it advances at least one 485 | character forward to prevent infinite looping. 486 | */ 487 | mode_buffer += lexeme; 488 | return lexeme.length || 1; 489 | } 490 | 491 | var language = getLanguage(name); 492 | if (!language) { 493 | throw new Error('Unknown language: "' + name + '"'); 494 | } 495 | 496 | compileLanguage(language); 497 | var top = continuation || language; 498 | var continuations = {}; // keep continuations for sub-languages 499 | var result = '', current; 500 | for(current = top; current !== language; current = current.parent) { 501 | if (current.className) { 502 | result = buildSpan(current.className, '', true) + result; 503 | } 504 | } 505 | var mode_buffer = ''; 506 | var relevance = 0; 507 | try { 508 | var match, count, index = 0; 509 | while (true) { 510 | top.terminators.lastIndex = index; 511 | match = top.terminators.exec(value); 512 | if (!match) 513 | break; 514 | count = processLexeme(value.substr(index, match.index - index), match[0]); 515 | index = match.index + count; 516 | } 517 | processLexeme(value.substr(index)); 518 | for(current = top; current.parent; current = current.parent) { // close dangling modes 519 | if (current.className) { 520 | result += spanEndTag; 521 | } 522 | } 523 | return { 524 | relevance: relevance, 525 | value: result, 526 | language: name, 527 | top: top 528 | }; 529 | } catch (e) { 530 | if (e.message && e.message.indexOf('Illegal') !== -1) { 531 | return { 532 | relevance: 0, 533 | value: escape(value) 534 | }; 535 | } else { 536 | throw e; 537 | } 538 | } 539 | } 540 | 541 | /* 542 | Highlighting with language detection. Accepts a string with the code to 543 | highlight. Returns an object with the following properties: 544 | 545 | - language (detected language) 546 | - relevance (int) 547 | - value (an HTML string with highlighting markup) 548 | - second_best (object with the same structure for second-best heuristically 549 | detected language, may be absent) 550 | 551 | */ 552 | function highlightAuto(text, languageSubset) { 553 | languageSubset = languageSubset || options.languages || objectKeys(languages); 554 | var result = { 555 | relevance: 0, 556 | value: escape(text) 557 | }; 558 | var second_best = result; 559 | languageSubset.filter(getLanguage).forEach(function(name) { 560 | var current = highlight(name, text, false); 561 | current.language = name; 562 | if (current.relevance > second_best.relevance) { 563 | second_best = current; 564 | } 565 | if (current.relevance > result.relevance) { 566 | second_best = result; 567 | result = current; 568 | } 569 | }); 570 | if (second_best.language) { 571 | result.second_best = second_best; 572 | } 573 | return result; 574 | } 575 | 576 | /* 577 | Post-processing of the highlighted markup: 578 | 579 | - replace TABs with something more useful 580 | - replace real line-breaks with '
' for non-pre containers 581 | 582 | */ 583 | function fixMarkup(value) { 584 | return !(options.tabReplace || options.useBR) 585 | ? value 586 | : value.replace(fixMarkupRe, function(match, p1) { 587 | if (options.useBR && match === '\n') { 588 | return '
'; 589 | } else if (options.tabReplace) { 590 | return p1.replace(/\t/g, options.tabReplace); 591 | } 592 | }); 593 | } 594 | 595 | function buildClassName(prevClassName, currentLang, resultLang) { 596 | var language = currentLang ? aliases[currentLang] : resultLang, 597 | result = [prevClassName.trim()]; 598 | 599 | if (!prevClassName.match(/\bhljs\b/)) { 600 | result.push('hljs'); 601 | } 602 | 603 | if (prevClassName.indexOf(language) === -1) { 604 | result.push(language); 605 | } 606 | 607 | return result.join(' ').trim(); 608 | } 609 | 610 | /* 611 | Applies highlighting to a DOM node containing code. Accepts a DOM node and 612 | two optional parameters for fixMarkup. 613 | */ 614 | function highlightBlock(block) { 615 | var node, originalStream, result, resultNode, text; 616 | var language = blockLanguage(block); 617 | 618 | if (isNotHighlighted(language)) 619 | return; 620 | 621 | if (options.useBR) { 622 | node = document.createElementNS('http://www.w3.org/1999/xhtml', 'div'); 623 | node.innerHTML = block.innerHTML.replace(/\n/g, '').replace(//g, '\n'); 624 | } else { 625 | node = block; 626 | } 627 | text = node.textContent; 628 | result = language ? highlight(language, text, true) : highlightAuto(text); 629 | 630 | originalStream = nodeStream(node); 631 | if (originalStream.length) { 632 | resultNode = document.createElementNS('http://www.w3.org/1999/xhtml', 'div'); 633 | resultNode.innerHTML = result.value; 634 | result.value = mergeStreams(originalStream, nodeStream(resultNode), text); 635 | } 636 | result.value = fixMarkup(result.value); 637 | 638 | block.innerHTML = result.value; 639 | block.className = buildClassName(block.className, language, result.language); 640 | block.result = { 641 | language: result.language, 642 | re: result.relevance 643 | }; 644 | if (result.second_best) { 645 | block.second_best = { 646 | language: result.second_best.language, 647 | re: result.second_best.relevance 648 | }; 649 | } 650 | } 651 | 652 | /* 653 | Updates highlight.js global options with values passed in the form of an object. 654 | */ 655 | function configure(user_options) { 656 | options = inherit(options, user_options); 657 | } 658 | 659 | /* 660 | Applies highlighting to all

..
blocks on a page. 661 | */ 662 | function initHighlighting() { 663 | if (initHighlighting.called) 664 | return; 665 | initHighlighting.called = true; 666 | 667 | var blocks = document.querySelectorAll('pre code'); 668 | ArrayProto.forEach.call(blocks, highlightBlock); 669 | } 670 | 671 | /* 672 | Attaches highlighting to the page load event. 673 | */ 674 | function initHighlightingOnLoad() { 675 | addEventListener('DOMContentLoaded', initHighlighting, false); 676 | addEventListener('load', initHighlighting, false); 677 | } 678 | 679 | function registerLanguage(name, language) { 680 | var lang = languages[name] = language(hljs); 681 | if (lang.aliases) { 682 | lang.aliases.forEach(function(alias) {aliases[alias] = name;}); 683 | } 684 | } 685 | 686 | function listLanguages() { 687 | return objectKeys(languages); 688 | } 689 | 690 | function getLanguage(name) { 691 | name = (name || '').toLowerCase(); 692 | return languages[name] || languages[aliases[name]]; 693 | } 694 | 695 | /* Interface definition */ 696 | 697 | hljs.highlight = highlight; 698 | hljs.highlightAuto = highlightAuto; 699 | hljs.fixMarkup = fixMarkup; 700 | hljs.highlightBlock = highlightBlock; 701 | hljs.configure = configure; 702 | hljs.initHighlighting = initHighlighting; 703 | hljs.initHighlightingOnLoad = initHighlightingOnLoad; 704 | hljs.registerLanguage = registerLanguage; 705 | hljs.listLanguages = listLanguages; 706 | hljs.getLanguage = getLanguage; 707 | hljs.inherit = inherit; 708 | 709 | // Common regexps 710 | hljs.IDENT_RE = '[a-zA-Z]\\w*'; 711 | hljs.UNDERSCORE_IDENT_RE = '[a-zA-Z_]\\w*'; 712 | hljs.NUMBER_RE = '\\b\\d+(\\.\\d+)?'; 713 | hljs.C_NUMBER_RE = '(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)'; // 0x..., 0..., decimal, float 714 | hljs.BINARY_NUMBER_RE = '\\b(0b[01]+)'; // 0b... 715 | hljs.RE_STARTERS_RE = '!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~'; 716 | 717 | // Common modes 718 | hljs.BACKSLASH_ESCAPE = { 719 | begin: '\\\\[\\s\\S]', relevance: 0 720 | }; 721 | hljs.APOS_STRING_MODE = { 722 | className: 'string', 723 | begin: '\'', end: '\'', 724 | illegal: '\\n', 725 | contains: [hljs.BACKSLASH_ESCAPE] 726 | }; 727 | hljs.QUOTE_STRING_MODE = { 728 | className: 'string', 729 | begin: '"', end: '"', 730 | illegal: '\\n', 731 | contains: [hljs.BACKSLASH_ESCAPE] 732 | }; 733 | hljs.PHRASAL_WORDS_MODE = { 734 | begin: /\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|like)\b/ 735 | }; 736 | hljs.COMMENT = function (begin, end, inherits) { 737 | var mode = hljs.inherit( 738 | { 739 | className: 'comment', 740 | begin: begin, end: end, 741 | contains: [] 742 | }, 743 | inherits || {} 744 | ); 745 | mode.contains.push(hljs.PHRASAL_WORDS_MODE); 746 | mode.contains.push({ 747 | className: 'doctag', 748 | begin: '(?:TODO|FIXME|NOTE|BUG|XXX):', 749 | relevance: 0 750 | }); 751 | return mode; 752 | }; 753 | hljs.C_LINE_COMMENT_MODE = hljs.COMMENT('//', '$'); 754 | hljs.C_BLOCK_COMMENT_MODE = hljs.COMMENT('/\\*', '\\*/'); 755 | hljs.HASH_COMMENT_MODE = hljs.COMMENT('#', '$'); 756 | hljs.NUMBER_MODE = { 757 | className: 'number', 758 | begin: hljs.NUMBER_RE, 759 | relevance: 0 760 | }; 761 | hljs.C_NUMBER_MODE = { 762 | className: 'number', 763 | begin: hljs.C_NUMBER_RE, 764 | relevance: 0 765 | }; 766 | hljs.BINARY_NUMBER_MODE = { 767 | className: 'number', 768 | begin: hljs.BINARY_NUMBER_RE, 769 | relevance: 0 770 | }; 771 | hljs.CSS_NUMBER_MODE = { 772 | className: 'number', 773 | begin: hljs.NUMBER_RE + '(' + 774 | '%|em|ex|ch|rem' + 775 | '|vw|vh|vmin|vmax' + 776 | '|cm|mm|in|pt|pc|px' + 777 | '|deg|grad|rad|turn' + 778 | '|s|ms' + 779 | '|Hz|kHz' + 780 | '|dpi|dpcm|dppx' + 781 | ')?', 782 | relevance: 0 783 | }; 784 | hljs.REGEXP_MODE = { 785 | className: 'regexp', 786 | begin: /\//, end: /\/[gimuy]*/, 787 | illegal: /\n/, 788 | contains: [ 789 | hljs.BACKSLASH_ESCAPE, 790 | { 791 | begin: /\[/, end: /\]/, 792 | relevance: 0, 793 | contains: [hljs.BACKSLASH_ESCAPE] 794 | } 795 | ] 796 | }; 797 | hljs.TITLE_MODE = { 798 | className: 'title', 799 | begin: hljs.IDENT_RE, 800 | relevance: 0 801 | }; 802 | hljs.UNDERSCORE_TITLE_MODE = { 803 | className: 'title', 804 | begin: hljs.UNDERSCORE_IDENT_RE, 805 | relevance: 0 806 | }; 807 | hljs.METHOD_GUARD = { 808 | // excludes method names from keyword processing 809 | begin: '\\.\\s*' + hljs.UNDERSCORE_IDENT_RE, 810 | relevance: 0 811 | }; 812 | 813 | hljs.registerLanguage('javascript', function(hljs) { 814 | var IDENT_RE = '[A-Za-z$_][0-9A-Za-z$_]*'; 815 | var KEYWORDS = { 816 | keyword: 817 | 'in of if for while finally var new function do return void else break catch ' + 818 | 'instanceof with throw case default try this switch continue typeof delete ' + 819 | 'let yield const export super debugger as async await static ' + 820 | // ECMAScript 6 modules import 821 | 'import from as' 822 | , 823 | literal: 824 | 'true false null undefined NaN Infinity', 825 | built_in: 826 | 'eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent ' + 827 | 'encodeURI encodeURIComponent escape unescape Object Function Boolean Error ' + 828 | 'EvalError InternalError RangeError ReferenceError StopIteration SyntaxError ' + 829 | 'TypeError URIError Number Math Date String RegExp Array Float32Array ' + 830 | 'Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array ' + 831 | 'Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require ' + 832 | 'module console window document Symbol Set Map WeakSet WeakMap Proxy Reflect ' + 833 | 'Promise' 834 | }; 835 | var EXPRESSIONS; 836 | var NUMBER = { 837 | className: 'number', 838 | variants: [ 839 | { begin: '\\b(0[bB][01]+)' }, 840 | { begin: '\\b(0[oO][0-7]+)' }, 841 | { begin: hljs.C_NUMBER_RE } 842 | ], 843 | relevance: 0 844 | }; 845 | var SUBST = { 846 | className: 'subst', 847 | begin: '\\$\\{', end: '\\}', 848 | keywords: KEYWORDS, 849 | contains: [] // defined later 850 | }; 851 | var TEMPLATE_STRING = { 852 | className: 'string', 853 | begin: '`', end: '`', 854 | contains: [ 855 | hljs.BACKSLASH_ESCAPE, 856 | SUBST 857 | ] 858 | }; 859 | SUBST.contains = [ 860 | hljs.APOS_STRING_MODE, 861 | hljs.QUOTE_STRING_MODE, 862 | TEMPLATE_STRING, 863 | NUMBER, 864 | hljs.REGEXP_MODE 865 | ] 866 | var PARAMS_CONTAINS = SUBST.contains.concat([ 867 | hljs.C_BLOCK_COMMENT_MODE, 868 | hljs.C_LINE_COMMENT_MODE 869 | ]); 870 | 871 | return { 872 | aliases: ['js', 'jsx'], 873 | keywords: KEYWORDS, 874 | contains: [ 875 | { 876 | className: 'meta', 877 | relevance: 10, 878 | begin: /^\s*['"]use (strict|asm)['"]/ 879 | }, 880 | { 881 | className: 'meta', 882 | begin: /^#!/, end: /$/ 883 | }, 884 | hljs.APOS_STRING_MODE, 885 | hljs.QUOTE_STRING_MODE, 886 | TEMPLATE_STRING, 887 | hljs.C_LINE_COMMENT_MODE, 888 | hljs.C_BLOCK_COMMENT_MODE, 889 | NUMBER, 890 | { // object attr container 891 | begin: /[{,]\s*/, relevance: 0, 892 | contains: [ 893 | { 894 | begin: IDENT_RE + '\\s*:', returnBegin: true, 895 | relevance: 0, 896 | contains: [{className: 'attr', begin: IDENT_RE, relevance: 0}] 897 | } 898 | ] 899 | }, 900 | { // "value" container 901 | begin: '(' + hljs.RE_STARTERS_RE + '|\\b(case|return|throw)\\b)\\s*', 902 | keywords: 'return throw case', 903 | contains: [ 904 | hljs.C_LINE_COMMENT_MODE, 905 | hljs.C_BLOCK_COMMENT_MODE, 906 | hljs.REGEXP_MODE, 907 | { 908 | className: 'function', 909 | begin: '(\\(.*?\\)|' + IDENT_RE + ')\\s*=>', returnBegin: true, 910 | end: '\\s*=>', 911 | contains: [ 912 | { 913 | className: 'params', 914 | variants: [ 915 | { 916 | begin: IDENT_RE 917 | }, 918 | { 919 | begin: /\(\s*\)/, 920 | }, 921 | { 922 | begin: /\(/, end: /\)/, 923 | excludeBegin: true, excludeEnd: true, 924 | keywords: KEYWORDS, 925 | contains: PARAMS_CONTAINS 926 | } 927 | ] 928 | } 929 | ] 930 | }, 931 | { // E4X / JSX 932 | begin: //, 933 | subLanguage: 'xml', 934 | contains: [ 935 | {begin: /<\w+\s*\/>/, skip: true}, 936 | { 937 | begin: /<\w+/, end: /(\/\w+|\w+\/)>/, skip: true, 938 | contains: [ 939 | {begin: /<\w+\s*\/>/, skip: true}, 940 | 'self' 941 | ] 942 | } 943 | ] 944 | } 945 | ], 946 | relevance: 0 947 | }, 948 | { 949 | className: 'function', 950 | beginKeywords: 'function', end: /\{/, excludeEnd: true, 951 | contains: [ 952 | hljs.inherit(hljs.TITLE_MODE, {begin: IDENT_RE}), 953 | { 954 | className: 'params', 955 | begin: /\(/, end: /\)/, 956 | excludeBegin: true, 957 | excludeEnd: true, 958 | contains: PARAMS_CONTAINS 959 | } 960 | ], 961 | illegal: /\[|%/ 962 | }, 963 | { 964 | begin: /\$[(.]/ // relevance booster for a pattern common to JS libs: `$(something)` and `$.something` 965 | }, 966 | hljs.METHOD_GUARD, 967 | { // ES6 class 968 | className: 'class', 969 | beginKeywords: 'class', end: /[{;=]/, excludeEnd: true, 970 | illegal: /[:"\[\]]/, 971 | contains: [ 972 | {beginKeywords: 'extends'}, 973 | hljs.UNDERSCORE_TITLE_MODE 974 | ] 975 | }, 976 | { 977 | beginKeywords: 'constructor', end: /\{/, excludeEnd: true 978 | } 979 | ], 980 | illegal: /#(?!!)/ 981 | }; 982 | }); 983 | 984 | return hljs; 985 | })); 986 | -------------------------------------------------------------------------------- /src/assets/js/hljs.js: -------------------------------------------------------------------------------- 1 | import hljs from 'highlight.js' 2 | import 'highlight.js/styles/monokai-sublime.css' 3 | hljs.initHighlightingOnLoad(); 4 | -------------------------------------------------------------------------------- /src/components/NotFound.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 20 | 21 | 54 | -------------------------------------------------------------------------------- /src/components/backEnd/Admin.vue: -------------------------------------------------------------------------------- 1 | 52 | 53 | 94 | 95 | 138 | -------------------------------------------------------------------------------- /src/components/backEnd/ArticleCreate.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 147 | 148 | 150 | -------------------------------------------------------------------------------- /src/components/backEnd/ArticleEdit.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 170 | 171 | 172 | -------------------------------------------------------------------------------- /src/components/backEnd/ArticleList.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 133 | 134 | 140 | -------------------------------------------------------------------------------- /src/components/backEnd/ClassList.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 182 | 183 | 189 | -------------------------------------------------------------------------------- /src/components/backEnd/Login.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 64 | 65 | 70 | -------------------------------------------------------------------------------- /src/components/backEnd/Reg.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 76 | 77 | 83 | -------------------------------------------------------------------------------- /src/components/fronted/About.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 33 | 34 | 72 | -------------------------------------------------------------------------------- /src/components/fronted/Article.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 38 | 39 | 80 | -------------------------------------------------------------------------------- /src/components/fronted/Front.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 32 | 33 | 72 | -------------------------------------------------------------------------------- /src/components/fronted/Home.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 79 | 80 | 163 | -------------------------------------------------------------------------------- /src/components/fronted/Tags.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 96 | 97 | 198 | -------------------------------------------------------------------------------- /src/components/fronted/vfooter.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 11 | 12 | 31 | -------------------------------------------------------------------------------- /src/components/fronted/vheader.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 41 | 42 | 116 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | // The Vue build version to load with the `import` command 2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias. 3 | import Vue from 'vue' 4 | import ElementUI from 'element-ui'; 5 | import 'element-ui/lib/theme-default/index.css'; 6 | import App from './App'; 7 | import 'assets/css/commen.css';//这里的样式可以覆盖index.css 8 | Vue.use(ElementUI); 9 | import router from './routes/index' 10 | import store from './store/index' 11 | /* eslint-disable no-new */ 12 | new Vue({ 13 | el: '#app', 14 | router, 15 | store, 16 | template: '', 17 | components: { App }//此处的components用在了上面的template里面用来编译 18 | }) 19 | 20 | -------------------------------------------------------------------------------- /src/routes/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | import store from '../store' 4 | 5 | Vue.use(VueRouter) 6 | import routes from './routes' 7 | // 滚动条滚回顶部 8 | const scrollBehavior =(to, from, savedPosition)=> { 9 | if (savedPosition) { 10 | return savedPosition 11 | } else { 12 | return { x: 0, y: 0 } 13 | } 14 | } 15 | const router = new VueRouter({ 16 | mode:'history', 17 | scrollBehavior, 18 | routes 19 | }) 20 | // 路由钩子 21 | router.beforeEach(({meta,path},from,next)=>{ 22 | store.dispatch('showProgress',0) 23 | // NProgress.start(); 24 | let {auth=true}=meta 25 | let isLogin = Boolean(store.state.token) 26 | 27 | /* 28 | 访问不需要权限的设置meta:false 29 | 注册也要设置成meta:false 30 | */ 31 | if(auth&&!isLogin&&path!=='/login'){ 32 | return next({path:'/login'}) 33 | } 34 | // 如果登录了以后再访问reg和login则路由到Home 35 | if(isLogin&&(path=='/login'||path=='/reg')){ 36 | return next({path:'/admin'}); 37 | } 38 | // 未登录的情况下访问reg则直接路由 39 | next(); 40 | }) 41 | // router.afterEach(route=>{ 42 | // NProgress.done(true); 43 | // }) 44 | export default router 45 | -------------------------------------------------------------------------------- /src/routes/routes.js: -------------------------------------------------------------------------------- 1 | import Reg from '../components/backEnd/Reg'; 2 | import Login from '../components/backEnd/Login'; 3 | import Admin from '../components/backEnd/Admin'; 4 | import ArticleCreate from '../components/backEnd/ArticleCreate' 5 | import ArticleList from '../components/backEnd/ArticleList' 6 | import ArticleEdit from '../components/backEnd/ArticleEdit' 7 | import ClassList from '../components/backEnd/ClassList' 8 | import Home from '../components/fronted/Home'; 9 | import Front from '../components/fronted/Front'; 10 | import About from '../components/fronted/About'; 11 | import Tags from '../components/fronted/tags'; 12 | import Article from '../components/fronted/Article'; 13 | import NotFound from '../components/NotFound' 14 | export default [ 15 | 16 | { 17 | path:'/reg', 18 | component:Reg, 19 | meta:{auth:false}, 20 | hidden:true 21 | }, 22 | 23 | { 24 | path:'/', 25 | component:Front,//这是文章页 26 | hidden:true, 27 | children:[ 28 | {path:'',redirect:'home', meta:{auth:false}}, 29 | {path:'home',component:Home, meta:{auth:false}}, 30 | {path:'about',component:About, meta:{auth:false}}, 31 | {path:'tags',component:Tags, meta:{auth:false}}, 32 | {path:'article/:id',component:Article, meta:{auth:false,scrollToTop: true}}, 33 | ] 34 | }, 35 | { 36 | path:'/login', 37 | component:Login, 38 | hidden:true 39 | }, 40 | { 41 | // 后台路由 42 | path:'/admin', 43 | component:Admin, 44 | name:'管理面板', 45 | iconCls: 'el-icon-message', 46 | children:[ 47 | { 48 | // 文章列表单独一个组件(可以删除并且编辑,编辑的时候需要跳转到另一个路由) 49 | path:'',hidden:true,redirect: {name:'文章管理'} 50 | }, 51 | { 52 | // 文章列表单独一个组件(可以删除并且编辑,编辑的时候需要跳转到另一个路由) 53 | path:'articleList',component:ArticleList,name:'文章管理' 54 | }, 55 | { 56 | // 创建文章单独一个组件 57 | path:'articleCreate',component:ArticleCreate,name:'创建文章',hidden:true 58 | }, 59 | { 60 | path:'articleEdit/:postId',component:ArticleEdit,hidden:true,name:"编辑文章" 61 | }, 62 | { 63 | path:'classList',component:ClassList,name:'分类管理' 64 | // 创建分类直接在分类列表里面出现弹层 65 | } 66 | ] 67 | }, 68 | { 69 | path:'*',component:NotFound,hidden:true 70 | } 71 | // 72 | 73 | // {path:'/404',component:NotFound} 74 | ] 75 | -------------------------------------------------------------------------------- /src/store/MsgAlert.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | export default function (message){ 3 | Vue.prototype.$message({ 4 | message, 5 | type:'error', 6 | showClose: true 7 | }) 8 | } 9 | -------------------------------------------------------------------------------- /src/store/actions.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import api from '../api' 3 | import router from '../routes' 4 | import MsgAlert from './MsgAlert' 5 | export default { 6 | // 后台注册 7 | UserReg({commit},data){ 8 | api.localReg(data) 9 | .then(({data})=>{ 10 | if(data.code==200){ 11 | commit('USER_REG',data.token) 12 | router.replace({path:'/admin'}) 13 | }else{ 14 | // 上一个catch处理了MongoError 15 | MsgAlert(data.message) 16 | } 17 | }) 18 | .catch((error)=>{ 19 | MsgAlert(error.toString()) 20 | }) 21 | }, 22 | // 后台登录 23 | UserLogin({commit},data){ 24 | api.localLogin(data) 25 | .then(({data})=>{ 26 | if(data.code==200){ 27 | // 找到用户 28 | commit('USER_SIGNIN',data.token) 29 | router.replace({path:'/admin/articleList'}) 30 | }else{ 31 | // 没找到用户或者密码不对 32 | MsgAlert(data.message) 33 | } 34 | }) 35 | .catch(error=>{ 36 | // 一般服务器连接不上这里就会报网络错误 37 | MsgAlert(error.toString()) 38 | }) 39 | }, 40 | UserLogout({commit}){ 41 | commit('USER_SIGNOUT'); 42 | router.push({path:'/login'}); 43 | }, 44 | showProgress({commit},number){ 45 | commit('SHOW_PROGRESS',number) 46 | }, 47 | changeHeadLine({commit},headline){ 48 | 49 | commit('HEAD_LINE',headline) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | Vue.use(Vuex); 4 | import state from './states' 5 | import actions from './actions' 6 | import mutations from './mutations' 7 | const store = new Vuex.Store({ 8 | state, 9 | mutations, 10 | actions 11 | }) 12 | export default store; 13 | -------------------------------------------------------------------------------- /src/store/mutations.js: -------------------------------------------------------------------------------- 1 | import {USER_SIGNIN,USER_SIGNOUT,USER_REG,SHOW_PROGRESS,HEAD_LINE} from './types' 2 | export default { 3 | [USER_REG](state,token){ 4 | localStorage.setItem('jwt',token); 5 | state.token = token; 6 | }, 7 | [USER_SIGNIN](state,token){ 8 | localStorage.setItem('jwt',token); 9 | state.token = token; 10 | }, 11 | [USER_SIGNOUT](state){ 12 | localStorage.removeItem('jwt'); 13 | state.token = null; 14 | }, 15 | [SHOW_PROGRESS](state,number){ 16 | state.progress = number 17 | }, 18 | [HEAD_LINE](state,headline){ 19 | state.headline = headline 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/store/states.js: -------------------------------------------------------------------------------- 1 | // 各种Store 2 | export default { 3 | token:isLoggedIn()||null, 4 | progress:0, 5 | headline:'' 6 | // 每次刷新页面或者再次访问的时候都会重新渲染状态, 7 | // 这里相当于给每次刷新重新设置初始值 8 | } 9 | 10 | function isLoggedIn(){ 11 | let token = localStorage.getItem('jwt'); 12 | if(token){ 13 | const payload = JSON.parse(window.atob(token.split('.')[1])); 14 | // 前端判断token是否过期,如果过期了访问时候会路由到login页面 15 | if(payload.exp>Date.now()/1000){ 16 | return token; 17 | } 18 | }else{ 19 | return false; 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/store/types.js: -------------------------------------------------------------------------------- 1 | export const USER_SIGNIN = 'USER_SIGNIN'; 2 | export const USER_SIGNOUT = "USER_SIGNOUT"; 3 | export const USER_REG = "USER_REG"; 4 | export const SHOW_PROGRESS = "SHOW_PROGRESS"; 5 | export const HEAD_LINE = 'HEAD_LINE' 6 | -------------------------------------------------------------------------------- /static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elva2596/vueBlog/e172b019024b1ef15f87dcaf5db40cc10e7f9df2/static/.gitkeep -------------------------------------------------------------------------------- /test/e2e/custom-assertions/elementCount.js: -------------------------------------------------------------------------------- 1 | // A custom Nightwatch assertion. 2 | // the name of the method is the filename. 3 | // can be used in tests like this: 4 | // 5 | // browser.assert.elementCount(selector, count) 6 | // 7 | // for how to write custom assertions see 8 | // http://nightwatchjs.org/guide#writing-custom-assertions 9 | exports.assertion = function (selector, count) { 10 | this.message = 'Testing if element <' + selector + '> has count: ' + count 11 | this.expected = count 12 | this.pass = function (val) { 13 | return val === this.expected 14 | } 15 | this.value = function (res) { 16 | return res.value 17 | } 18 | this.command = function (cb) { 19 | var self = this 20 | return this.api.execute(function (selector) { 21 | return document.querySelectorAll(selector).length 22 | }, [selector], function (res) { 23 | cb.call(self, res) 24 | }) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/e2e/nightwatch.conf.js: -------------------------------------------------------------------------------- 1 | require('babel-register') 2 | var config = require('../../config') 3 | 4 | // http://nightwatchjs.org/guide#settings-file 5 | module.exports = { 6 | src_folders: ['test/e2e/specs'], 7 | output_folder: 'test/e2e/reports', 8 | custom_assertions_path: ['test/e2e/custom-assertions'], 9 | 10 | selenium: { 11 | start_process: true, 12 | server_path: 'node_modules/selenium-server/lib/runner/selenium-server-standalone-2.53.1.jar', 13 | host: '127.0.0.1', 14 | port: 4444, 15 | cli_args: { 16 | 'webdriver.chrome.driver': require('chromedriver').path 17 | } 18 | }, 19 | 20 | test_settings: { 21 | default: { 22 | selenium_port: 4444, 23 | selenium_host: 'localhost', 24 | silent: true, 25 | globals: { 26 | devServerURL: 'http://localhost:' + (process.env.PORT || config.dev.port) 27 | } 28 | }, 29 | 30 | chrome: { 31 | desiredCapabilities: { 32 | browserName: 'chrome', 33 | javascriptEnabled: true, 34 | acceptSslCerts: true 35 | } 36 | }, 37 | 38 | firefox: { 39 | desiredCapabilities: { 40 | browserName: 'firefox', 41 | javascriptEnabled: true, 42 | acceptSslCerts: true 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/e2e/runner.js: -------------------------------------------------------------------------------- 1 | // 1. start the dev server using production config 2 | process.env.NODE_ENV = 'testing' 3 | var server = require('../../build/dev-server.js') 4 | 5 | // 2. run the nightwatch test suite against it 6 | // to run in additional browsers: 7 | // 1. add an entry in test/e2e/nightwatch.conf.json under "test_settings" 8 | // 2. add it to the --env flag below 9 | // or override the environment flag, for example: `npm run e2e -- --env chrome,firefox` 10 | // For more information on Nightwatch's config file, see 11 | // http://nightwatchjs.org/guide#settings-file 12 | var opts = process.argv.slice(2) 13 | if (opts.indexOf('--config') === -1) { 14 | opts = opts.concat(['--config', 'test/e2e/nightwatch.conf.js']) 15 | } 16 | if (opts.indexOf('--env') === -1) { 17 | opts = opts.concat(['--env', 'chrome']) 18 | } 19 | 20 | var spawn = require('cross-spawn') 21 | var runner = spawn('./node_modules/.bin/nightwatch', opts, { stdio: 'inherit' }) 22 | 23 | runner.on('exit', function (code) { 24 | server.close() 25 | process.exit(code) 26 | }) 27 | 28 | runner.on('error', function (err) { 29 | server.close() 30 | throw err 31 | }) 32 | -------------------------------------------------------------------------------- /test/e2e/specs/test.js: -------------------------------------------------------------------------------- 1 | // For authoring Nightwatch tests, see 2 | // http://nightwatchjs.org/guide#usage 3 | 4 | module.exports = { 5 | 'default e2e tests': function (browser) { 6 | // automatically uses dev Server port from /config.index.js 7 | // default: http://localhost:8080 8 | // see nightwatch.conf.js 9 | const devServer = browser.globals.devServerURL 10 | 11 | browser 12 | .url(devServer) 13 | .waitForElementVisible('#app', 5000) 14 | .assert.elementPresent('.hello') 15 | .assert.containsText('h1', 'Welcome to Your Vue.js App') 16 | .assert.elementCount('img', 1) 17 | .end() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/unit/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "globals": { 6 | "expect": true, 7 | "sinon": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/unit/index.js: -------------------------------------------------------------------------------- 1 | // Polyfill fn.bind() for PhantomJS 2 | /* eslint-disable no-extend-native */ 3 | Function.prototype.bind = require('function-bind') 4 | 5 | // require all test files (files that ends with .spec.js) 6 | const testsContext = require.context('./specs', true, /\.spec$/) 7 | testsContext.keys().forEach(testsContext) 8 | 9 | // require all src files except main.js for coverage. 10 | // you can also change this to match only the subset of files that 11 | // you want coverage for. 12 | const srcContext = require.context('src', true, /^\.\/(?!main(\.js)?$)/) 13 | srcContext.keys().forEach(srcContext) 14 | -------------------------------------------------------------------------------- /test/unit/karma.conf.js: -------------------------------------------------------------------------------- 1 | // This is a karma config file. For more details see 2 | // http://karma-runner.github.io/0.13/config/configuration-file.html 3 | // we are also using it with karma-webpack 4 | // https://github.com/webpack/karma-webpack 5 | 6 | var path = require('path') 7 | var merge = require('webpack-merge') 8 | var baseConfig = require('../../build/webpack.base.conf') 9 | var utils = require('../../build/utils') 10 | var webpack = require('webpack') 11 | var projectRoot = path.resolve(__dirname, '../../') 12 | 13 | var webpackConfig = merge(baseConfig, { 14 | // use inline sourcemap for karma-sourcemap-loader 15 | module: { 16 | loaders: utils.styleLoaders() 17 | }, 18 | devtool: '#inline-source-map', 19 | vue: { 20 | loaders: { 21 | js: 'babel-loader' 22 | } 23 | }, 24 | plugins: [ 25 | new webpack.DefinePlugin({ 26 | 'process.env': require('../../config/test.env') 27 | }) 28 | ] 29 | }) 30 | 31 | // no need for app entry during tests 32 | delete webpackConfig.entry 33 | 34 | // Use babel for test files too 35 | webpackConfig.module.loaders.some(function (loader, i) { 36 | if (/^babel(-loader)?$/.test(loader.loader)) { 37 | loader.include.push(path.resolve(projectRoot, 'test/unit')) 38 | return true 39 | } 40 | }) 41 | 42 | module.exports = function (config) { 43 | config.set({ 44 | // to run in additional browsers: 45 | // 1. install corresponding karma launcher 46 | // http://karma-runner.github.io/0.13/config/browsers.html 47 | // 2. add it to the `browsers` array below. 48 | browsers: ['PhantomJS'], 49 | frameworks: ['mocha', 'sinon-chai'], 50 | reporters: ['spec', 'coverage'], 51 | files: ['./index.js'], 52 | preprocessors: { 53 | './index.js': ['webpack', 'sourcemap'] 54 | }, 55 | webpack: webpackConfig, 56 | webpackMiddleware: { 57 | noInfo: true 58 | }, 59 | coverageReporter: { 60 | dir: './coverage', 61 | reporters: [ 62 | { type: 'lcov', subdir: '.' }, 63 | { type: 'text-summary' } 64 | ] 65 | } 66 | }) 67 | } 68 | -------------------------------------------------------------------------------- /test/unit/specs/Hello.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Hello from 'src/components/Hello' 3 | 4 | describe('Hello.vue', () => { 5 | it('should render correct contents', () => { 6 | const vm = new Vue({ 7 | el: document.createElement('div'), 8 | render: (h) => h(Hello) 9 | }) 10 | expect(vm.$el.querySelector('.hello h1').textContent) 11 | .to.equal('Welcome to Your Vue.js App') 12 | }) 13 | }) 14 | --------------------------------------------------------------------------------