├── .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 | 
16 |
17 |
18 | #### 移动端前台演示
19 | 
20 |
21 | #### 后台管理演示
22 | 
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 |
2 |
3 |
4 |
5 |
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 += '' + tag(node) + '>';
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: /, end: /(\/\w+|\w+\/)>/,
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 |
2 |
3 |
4 |
哎呀,您走远了 !
5 |
请重回主页
6 |
7 |
8 |
9 |
10 |
11 |
20 |
21 |
54 |
--------------------------------------------------------------------------------
/src/components/backEnd/Admin.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | ADMIN
9 |
10 |
11 |
12 |
13 | 更多操作
14 |
15 |
16 | 博客首页
17 | 退出登录
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | {{item.name}}
30 | {{child.name}}
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | {{currentPathNameParent}}
42 | {{currentPathName}}
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
94 |
95 |
138 |
--------------------------------------------------------------------------------
/src/components/backEnd/ArticleCreate.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | {{btnText}}
33 | 取消
34 |
35 |
36 |
37 |
38 |
39 |
40 |
147 |
148 |
150 |
--------------------------------------------------------------------------------
/src/components/backEnd/ArticleEdit.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | {{btnText}}
33 | 返回
34 |
35 |
36 |
37 |
38 |
39 |
40 |
170 |
171 |
172 |
--------------------------------------------------------------------------------
/src/components/backEnd/ArticleList.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 创建文章
6 |
7 |
8 |
9 |
10 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | 查看
20 | 编辑
21 | 删除
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
133 |
134 |
140 |
--------------------------------------------------------------------------------
/src/components/backEnd/ClassList.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 添加分类
6 |
7 |
8 |
9 |
10 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | 编辑
20 | 删除
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | 取 消
37 | {{btnText}}
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
182 |
183 |
189 |
--------------------------------------------------------------------------------
/src/components/backEnd/Login.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 欢迎登录后台管理系统
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | 登录
13 | 注册
14 |
15 |
16 |
17 |
18 |
19 |
64 |
65 |
70 |
--------------------------------------------------------------------------------
/src/components/backEnd/Reg.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 系统注册
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | 注册
16 | 登录
17 |
18 |
19 |
20 |
21 |
22 |
76 |
77 |
83 |
--------------------------------------------------------------------------------
/src/components/fronted/About.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
平凡之路
5 |
一个有强迫症的前端患者
6 |
Github 掘金简书
7 |
关于网站
8 |
本站服务端采用 express + mongoDB 搭建, 客户端采用Vue2全家桶
9 |
主要功能包括: 后台登录, 注册, 管理, 标签, 文章, 支持markdown语法编辑, 支持代码高亮
10 |
主要技术栈: express, mongoolass, vue2, vue2-router, vuex, axios, es6, element ui
11 |
12 |
13 |
14 |
15 |
33 |
34 |
72 |
--------------------------------------------------------------------------------
/src/components/fronted/Article.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 |
12 |
13 |
38 |
39 |
80 |
--------------------------------------------------------------------------------
/src/components/fronted/Front.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
32 |
33 |
72 |
--------------------------------------------------------------------------------
/src/components/fronted/Home.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
14 |
15 |
18 |
19 |
20 |
21 |
22 |
23 |
79 |
80 |
163 |
--------------------------------------------------------------------------------
/src/components/fronted/Tags.vue:
--------------------------------------------------------------------------------
1 |
2 |
29 |
30 |
31 |
96 |
97 |
198 |
--------------------------------------------------------------------------------
/src/components/fronted/vfooter.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
11 |
12 |
31 |
--------------------------------------------------------------------------------
/src/components/fronted/vheader.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 |
13 |
14 | {{finalheadline}}
15 |
16 |
17 |
18 |
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 |
--------------------------------------------------------------------------------