├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── app ├── controllers │ ├── home.js │ └── search.js ├── index.js ├── middlewares │ ├── error.js │ └── log.js ├── models │ └── index.js ├── router.js └── services │ ├── cache.js │ ├── logger.js │ ├── push.js │ └── render.js ├── babel.config.js ├── bin └── index.js ├── config.json ├── package.json ├── public ├── favicon.ico └── index.html ├── src ├── App.vue ├── api.js ├── components │ ├── Button.vue │ ├── Card.vue │ ├── Dialog.vue │ ├── Input.vue │ └── Mask.vue ├── entry.client.js ├── entry.server.js ├── main.js ├── pages │ ├── Home.vue │ └── Search.vue ├── router.js └── store.js └── vue.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /tmp-bundle 4 | /dist 5 | 6 | # Local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | 15 | # Editor directories and files 16 | .idea 17 | .vscode 18 | *.suo 19 | *.ntvs* 20 | *.njsproj 21 | *.sln 22 | *.sw* 23 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /tmp-bundle 4 | /src 5 | 6 | # Dev config files 7 | /babel.config.js 8 | /vue.config.js 9 | 10 | # Local env files 11 | .env.local 12 | .env.*.local 13 | 14 | # Log files 15 | npm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | 19 | # Editor directories and files 20 | .idea 21 | .vscode 22 | *.suo 23 | *.ntvs* 24 | *.njsproj 25 | *.sln 26 | *.sw* 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2019 Jimkwan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # KPush 2 | 3 | [![](https://img.shields.io/npm/v/kpush.svg)](https://www.npmjs.com/package/kpush) 4 | 5 | 定制个人专属的Kindle电子书推送服务 6 | 7 | ## 安装 8 | 9 | KPush需要安装版本8.0以上的Node环境,[相关信息请戳这里](https://nodejs.org/en/download/current/) 10 | 11 | ```bash 12 | $ npm i -g kpush 13 | ``` 14 | 15 | 当然,也可以直接通过npx安装使用,具体配置看下面使用帮助 16 | 17 | ```bash 18 | $ npx kpush -o localhost -p 8081 19 | ``` 20 | 21 | ## 使用 22 | 23 | 帮助: 24 | 25 | ```bash 26 | Usage: kpush [options] 27 | 28 | Options: 29 | -V, --version output the version number 30 | -c, --config set kpush custom config 31 | -o, --host set kpush server listening host 32 | -p, --port set kpush server listening port 33 | -s, --smtp set stmp server of pushing mail 34 | -u, --user set user of pushing mail 35 | -w, --pass set password of pushing mail 36 | -k, --kindle set user of kindle received mail 37 | -h, --help output usage information 38 | ``` 39 | 40 | 所有设置都会自动保存,再次使用时无需再进行设置 41 | 42 | 配置设置并启动: 43 | 44 | ```bash 45 | # 可以只设置其中几项 46 | $ kpush -o localhost -p 8081 -s smtp.163.com -u test@163.com -w test -k test@kindle.cn 47 | ``` 48 | 49 | 以自定义配置启动: 50 | 51 | ```bash 52 | # 请提供配置的绝对路径,以自定义配置启动时将忽略其他设置选项 53 | $ kpush -c /usr/local/kpush/config.json 54 | ``` 55 | 56 | 配置格式如下,字段含义见帮助: 57 | 58 | ```js 59 | { 60 | "host": "localhost", 61 | "port": "8081", 62 | "smtp": "smtp.163.com", 63 | "user": "test@163.com", 64 | "pass": "test", 65 | "kindle": "test@kindle.cn" 66 | } 67 | ``` 68 | 69 | 启动后,浏览器访问KPush服务器监听端口即可使用,推荐移动端进行访问 70 | 71 | ## mobi源更换 72 | 73 | 这里默认用了[Z-Library](https://3lib.net)作为mobi电子书源,感谢一下。若更换其他mobi源,请自行fork以后重写`app/models/index.js`中getList和getUrl方法,约定见注释 74 | -------------------------------------------------------------------------------- /app/controllers/home.js: -------------------------------------------------------------------------------- 1 | const cache = require('../services/cache') 2 | const render = require('../services/render') 3 | 4 | module.exports = { 5 | async get(ctx) { 6 | let html = cache.get(ctx.url) 7 | if (!html) { 8 | html = await render({ 9 | title: 'KPush', 10 | url: ctx.url 11 | }) 12 | cache.set(ctx.url, html) 13 | } 14 | ctx.body = html 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/controllers/search.js: -------------------------------------------------------------------------------- 1 | const model = require('../models') 2 | const push = require('../services/push') 3 | const cache = require('../services/cache') 4 | const render = require('../services/render') 5 | 6 | module.exports = { 7 | async get(ctx) { 8 | let html = cache.get(ctx.url) 9 | if (!html) { 10 | html = await render({ 11 | title: 'KPush - 搜索结果', 12 | url: ctx.url 13 | }) 14 | cache.set(ctx.url, html) 15 | } 16 | ctx.body = html 17 | }, 18 | async search(ctx) { 19 | let data = cache.get(ctx.url) 20 | if (!data) { 21 | data = await model.getList(ctx.query.query) 22 | cache.set(ctx.url, data) 23 | } 24 | ctx.body = data 25 | }, 26 | async push(ctx) { 27 | let url = cache.get(ctx.request.body.id) 28 | if (!url) { 29 | url = await model.getUrl(ctx.request.body.id) 30 | cache.set(ctx.request.body.id, url) 31 | } 32 | ctx.body = await push(url) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | const Koa = require('koa') 2 | const path = require('path') 3 | const router = require('./router') 4 | const static = require('koa-static') 5 | const parser = require('koa-bodyparser') 6 | const Logger = require('./services/logger') 7 | 8 | const app = new Koa() 9 | 10 | app.context.log = new Logger() 11 | 12 | app.use(static(path.resolve(__dirname, '../dist'))) 13 | app.use(parser()) 14 | app.use(router) 15 | 16 | if (module.parent) { 17 | module.exports = app 18 | } else { 19 | if (process.env.APP_ENV === 'dev') { 20 | const cors = require('koa2-cors') 21 | app.use(cors({ origin: 'http://localhost:8080' })) 22 | } 23 | app.listen(8081, () => app.context.log.w(`Dev server is listening at http://localhost:8081`)) 24 | } -------------------------------------------------------------------------------- /app/middlewares/error.js: -------------------------------------------------------------------------------- 1 | module.exports = async function error(ctx, next) { 2 | try { 3 | await next() 4 | } catch (err) { 5 | ctx.status = 500 6 | ctx.log.e(err.toString()) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /app/middlewares/log.js: -------------------------------------------------------------------------------- 1 | module.exports = async function log(ctx, next) { 2 | const start = Date.now() 3 | await next() 4 | const reqLog = `${ctx.method} ${ctx.status} ${ctx.url} ${Date.now() - start}ms` 5 | if (ctx.status < 300) { 6 | ctx.log.i(reqLog) 7 | } else if (ctx.status >= 300 && ctx.status < 400) { 8 | ctx.log.w(reqLog) 9 | } else { 10 | ctx.log.e(reqLog) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/models/index.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios') 2 | const cheerio = require('cheerio') 3 | 4 | module.exports = { 5 | /** 6 | * 获取mobi电子书搜索列表 7 | * 8 | * @param {string} query - 查询关键词 9 | * @return {array} - 返回电子书列表 10 | * 11 | * 返回数组元素为mobi对象,包含3个字段,均为string 12 | * mobi.id - mobi电子书唯一id 13 | * mobi.desc - mobi电子书描述(标题、简介等) 14 | * mobi.cover? - mobi电子书封面(可选) 15 | */ 16 | async getList(query) { 17 | const res = await axios.get(`https://3lib.net/s/`, { 18 | headers: { 19 | 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1' 20 | }, 21 | params: { 22 | 'q': query, 23 | 'extensions[]': 'mobi' 24 | } 25 | }) 26 | const $ = cheerio.load(res.data) 27 | const $li = $('#searchResultBox .resItemBox') 28 | const list = [] 29 | for(let i = 1; i < $li.length; i++) { 30 | const $td = $li.eq(i).find('td') 31 | const $item = $td.eq(2).find('a') 32 | const $cover = $td.eq(0).find('img') 33 | const cover = $cover.attr('data-src') 34 | list.push({ 35 | id: $item.attr('href'), 36 | desc: $item.text(), 37 | cover: cover !== '/img/cover-not-exists.png' ? cover : '' 38 | }) 39 | } 40 | return list 41 | }, 42 | /** 43 | * 通过id换取下载链接 44 | * 45 | * @param {string} id - mobi电子书唯一id 46 | * @return {string} - 返回mobi电子书下载用url 47 | */ 48 | async getUrl(id) { 49 | const res = await axios.get(`https://3lib.net${id}`, { 50 | headers: { 51 | 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1' 52 | } 53 | }) 54 | const $ = cheerio.load(res.data) 55 | return `https://3lib.net${$('.dlButton').attr('href')}` 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/router.js: -------------------------------------------------------------------------------- 1 | const router = require('koa-router')() 2 | const logMiddle = require('./middlewares/log') 3 | const errorMiddle = require('./middlewares/error') 4 | const homeCrtller = require('./controllers/home') 5 | const searchCrtller = require('./controllers/search') 6 | 7 | router.use(logMiddle) 8 | router.use(errorMiddle) 9 | 10 | router.get('/', homeCrtller.get) 11 | router.get('/search', searchCrtller.get) 12 | 13 | router.get('/api/search', searchCrtller.search) 14 | router.post('/api/push', searchCrtller.push) 15 | 16 | module.exports = router.routes() -------------------------------------------------------------------------------- /app/services/cache.js: -------------------------------------------------------------------------------- 1 | const LRU = require('lru-cache') 2 | 3 | module.exports = new LRU({ 4 | max: 100, 5 | maxAge: 30 * 60 * 1000 // 半小时缓存 6 | }) 7 | -------------------------------------------------------------------------------- /app/services/logger.js: -------------------------------------------------------------------------------- 1 | const LEVEL = { 2 | DBG: 0, 3 | INF: 1, 4 | WRN: 2, 5 | ERR: 3 6 | } 7 | const COLOR = { 8 | DBG: '\x1b[35m', 9 | INF: '\x1b[37m', 10 | WRN: '\x1b[33m', 11 | ERR: '\x1b[31m', 12 | RST: '\x1b[0m' 13 | } 14 | 15 | class Logger { 16 | constructor (level) { 17 | this.level = typeof level === 'number' ? level : 1 18 | } 19 | formatLog(type, msg) { 20 | if (this.level > LEVEL[type]) return 21 | const now = new Date() 22 | console.log(`${COLOR[type]}${now.toLocaleDateString()} ${now.toLocaleTimeString()} [${type}] ${msg}${COLOR.RST}`) 23 | } 24 | d(msg) { this.formatLog('DBG', msg) } 25 | i(msg) { this.formatLog('INF', msg) } 26 | w(msg) { this.formatLog('WRN', msg) } 27 | e(msg) { this.formatLog('ERR', msg) } 28 | } 29 | 30 | module.exports = Logger 31 | -------------------------------------------------------------------------------- /app/services/push.js: -------------------------------------------------------------------------------- 1 | const config = require('../../config') 2 | const nodemailer = require('nodemailer') 3 | 4 | const transporter = nodemailer.createTransport({ 5 | host : config.smtp, 6 | secureConnection: true, 7 | port: 465, 8 | auth : { 9 | user : config.user, 10 | pass : config.pass 11 | } 12 | }) 13 | 14 | module.exports = path => transporter.sendMail({ 15 | from: `noreply < ${config.user} >`, 16 | to: config.kindle, 17 | subject: 'Convert', 18 | text: `Pushing to kindle from ${path}`, 19 | attachments: [{ 20 | path: encodeURI(path), 21 | encoding: 'base64', 22 | contentType: 'application/x-mobipocket-ebook' 23 | }] 24 | }) 25 | -------------------------------------------------------------------------------- /app/services/render.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const { createBundleRenderer } = require('vue-server-renderer') 4 | 5 | const bundle = require('../../dist/vue-ssr-server-bundle.json') 6 | const clientManifest = require('../../dist/vue-ssr-client-manifest.json') 7 | const template = fs.readFileSync(path.resolve(__dirname, '../../public/index.html'), 'utf-8') 8 | .replace('
', '') 9 | 10 | const renderer = createBundleRenderer(bundle, { 11 | template, 12 | clientManifest, 13 | runInNewContext: false 14 | }) 15 | 16 | module.exports = ctx => renderer.renderToString(ctx) 17 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /bin/index.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | const fs = require('fs') 4 | const path = require('path') 5 | const program = require('commander') 6 | const package = require('../package') 7 | 8 | program.version(package.version) 9 | .option('-c, --config ', 'set kpush custom config') 10 | .option('-o, --host ', 'set kpush server listening host') 11 | .option('-p, --port ', 'set kpush server listening port') 12 | .option('-s, --smtp ', 'set stmp server of pushing mail') 13 | .option('-u, --user ', 'set user of pushing mail') 14 | .option('-w, --pass ', 'set password of pushing mail') 15 | .option('-k, --kindle ', 'set user of kindle received mail') 16 | .parse(process.argv) 17 | 18 | let config = require('../config') 19 | if (program.config) { 20 | try { 21 | config = require(program.config) 22 | } catch (err) { 23 | console.log("\n error: can't load config at `%s'\n", program.config) 24 | } 25 | } else { 26 | config.host = program.host || config.host 27 | config.port = program.port || config.port 28 | config.smtp = program.smtp || config.smtp 29 | config.user = program.user || config.user 30 | config.pass = program.pass || config.pass 31 | config.kindle = program.kindle || config.kindle 32 | } 33 | fs.writeFileSync(path.join(__dirname, '../config.json'), JSON.stringify(config)) 34 | 35 | const app = require('../app') 36 | 37 | app.listen(config.port, config.host, 38 | () => app.context.log.i(`Server is listening at http://${config.host}:${config.port}`)) -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "host": "localhost", 3 | "port": "8081", 4 | "smtp": "smtp.163.com", 5 | "user": "test@163.com", 6 | "pass": "test", 7 | "kindle": "test@kindle.cn" 8 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kpush", 3 | "version": "2.2.0", 4 | "description": "A push server for kindle based on 3lib.net", 5 | "main": "app/index.js", 6 | "bin": { 7 | "kpush": "bin/index.js" 8 | }, 9 | "scripts": { 10 | "start": "cross-env APP_ENV=dev node app", 11 | "serve": "vue-cli-service serve", 12 | "build": "npm run build:server && npm run rename && npm run build:client && npm run rename:back && npm run remove", 13 | "build:client": "vue-cli-service build", 14 | "build:server": "cross-env WEBPACK_TARGET=node vue-cli-service build", 15 | "rename": "mv dist/vue-ssr-server-bundle.json tmp-bundle", 16 | "rename:back": "mv tmp-bundle dist/vue-ssr-server-bundle.json", 17 | "remove": "rm dist/index.html", 18 | "lint": "vue-cli-service lint", 19 | "pub": "npm run lint && npm run build && npm publish" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/Siubaak/KPush.git" 24 | }, 25 | "keywords": [ 26 | "kindle", 27 | "push", 28 | "mobi" 29 | ], 30 | "author": "Siubaak", 31 | "license": "MIT", 32 | "bugs": { 33 | "url": "https://github.com/Siubaak/KPush/issues" 34 | }, 35 | "homepage": "https://github.com/Siubaak/KPush#README", 36 | "dependencies": { 37 | "axios": "^0.21.1", 38 | "cheerio": "^1.0.0-rc.2", 39 | "commander": "^2.19.0", 40 | "core-js": "^2.6.1", 41 | "koa": "^2.6.2", 42 | "koa-bodyparser": "^4.2.1", 43 | "koa-router": "^7.4.0", 44 | "koa-static": "^5.0.0", 45 | "lru-cache": "^5.1.1", 46 | "nodemailer": "^6.6.1", 47 | "nprogress": "^0.2.0", 48 | "vue": "^2.5.21", 49 | "vue-router": "^3.0.2", 50 | "vue-server-renderer": "^2.5.21", 51 | "vuex": "^3.0.1", 52 | "vuex-router-sync": "^5.0.0" 53 | }, 54 | "devDependencies": { 55 | "@vue/cli-plugin-babel": "^3.2.2", 56 | "@vue/cli-plugin-eslint": "^3.2.2", 57 | "@vue/cli-service": "^3.2.2", 58 | "babel-eslint": "^10.0.1", 59 | "cross-env": "^5.2.0", 60 | "eslint": "^5.11.1", 61 | "eslint-plugin-vue": "^5.0.0", 62 | "koa2-cors": "^2.0.6", 63 | "less": "^3.9.0", 64 | "less-loader": "^4.1.0", 65 | "lodash.merge": "^4.6.1", 66 | "vue-template-compiler": "^2.5.21", 67 | "webpack-node-externals": "^1.7.2" 68 | }, 69 | "eslintConfig": { 70 | "root": true, 71 | "env": { 72 | "node": true 73 | }, 74 | "extends": [ 75 | "plugin:vue/essential", 76 | "eslint:recommended" 77 | ], 78 | "rules": {}, 79 | "parserOptions": { 80 | "parser": "babel-eslint" 81 | } 82 | }, 83 | "postcss": { 84 | "plugins": { 85 | "autoprefixer": {} 86 | } 87 | }, 88 | "browserslist": [ 89 | "> 1%", 90 | "last 2 versions", 91 | "not ie <= 8" 92 | ] 93 | } 94 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Siubaak/KPush/d6beebee66b2300c0bd0f4432b998e3595595ace/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{title}} 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | -------------------------------------------------------------------------------- /src/api.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | export function getList(query) { 4 | return axios.get('/api/search', { params: { query } }) 5 | } 6 | 7 | export function push(id) { 8 | return axios.post('/api/push', { id }) 9 | } -------------------------------------------------------------------------------- /src/components/Button.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 24 | -------------------------------------------------------------------------------- /src/components/Card.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 29 | 30 | 72 | -------------------------------------------------------------------------------- /src/components/Dialog.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 31 | 32 | 47 | -------------------------------------------------------------------------------- /src/components/Input.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 27 | 28 | 52 | -------------------------------------------------------------------------------- /src/components/Mask.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 30 | 31 | 41 | -------------------------------------------------------------------------------- /src/entry.client.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import progress from 'nprogress' 3 | import { createApp } from './main' 4 | 5 | progress.configure({ showSpinner: false }) 6 | 7 | axios.interceptors.request.use(config => { 8 | progress.start() 9 | return config 10 | }, error => { 11 | progress.done() 12 | return Promise.reject(error) 13 | }) 14 | 15 | axios.interceptors.response.use(response => { 16 | progress.done() 17 | return response 18 | }, error => { 19 | progress.done() 20 | return Promise.reject(error) 21 | }) 22 | 23 | const { app, router, store } = createApp() 24 | 25 | if (window.__INITIAL_STATE__) { 26 | store.replaceState(window.__INITIAL_STATE__) 27 | } 28 | 29 | router.onReady(() => { 30 | router.beforeResolve((to, from, next) => { 31 | progress.start() 32 | 33 | const matched = router.getMatchedComponents(to) 34 | const prevMatched = router.getMatchedComponents(from) 35 | 36 | let diffed = false 37 | const activated = matched.filter((c, i) => { 38 | return diffed || (diffed = (prevMatched[i] !== c)) 39 | }) 40 | 41 | if (!activated.length) { 42 | return next() 43 | } 44 | 45 | Promise.all(activated.map(c => { 46 | if (c.asyncData) { 47 | return c.asyncData({ store, route: to }) 48 | } 49 | })).then(next).catch(next) 50 | }) 51 | 52 | router.afterEach(() => progress.done()) 53 | 54 | app.$mount('#app') 55 | }) 56 | -------------------------------------------------------------------------------- /src/entry.server.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import config from '../config' 3 | import { createApp } from './main' 4 | 5 | if (process.env.APP_ENV === 'dev') { 6 | axios.defaults.baseURL = 'http://localhost:8081' 7 | } else { 8 | axios.defaults.baseURL = `http://${config.host}:${config.port}` 9 | } 10 | 11 | export default ctx => { 12 | return new Promise((resolve, reject) => { 13 | const { app, router, store } = createApp() 14 | 15 | router.push(ctx.url) 16 | 17 | router.onReady(() => { 18 | const matchedComponents = router.getMatchedComponents() 19 | if (!matchedComponents.length) { 20 | return reject({ code: 404 }) 21 | } 22 | 23 | Promise.all(matchedComponents.map(Component => { 24 | if (Component.asyncData) { 25 | return Component.asyncData({ 26 | store, 27 | route: router.currentRoute 28 | }) 29 | } 30 | })).then(() => { 31 | ctx.state = store.state 32 | resolve(app) 33 | }).catch(reject) 34 | }, reject) 35 | }) 36 | } -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import { createRouter } from './router' 4 | import { createStore } from './store' 5 | import { sync } from 'vuex-router-sync' 6 | import 'nprogress/nprogress.css' 7 | 8 | Vue.config.productionTip = false 9 | 10 | export function createApp () { 11 | const router = createRouter() 12 | const store = createStore() 13 | 14 | sync(store, router) 15 | 16 | const app = new Vue({ 17 | router, 18 | store, 19 | render: h => h(App) 20 | }) 21 | 22 | return { app, router, store } 23 | } 24 | -------------------------------------------------------------------------------- /src/pages/Home.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 33 | 34 | 54 | -------------------------------------------------------------------------------- /src/pages/Search.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 84 | 85 | 124 | -------------------------------------------------------------------------------- /src/router.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import Home from './pages/Home' 4 | import Search from './pages/Search' 5 | 6 | Vue.use(Router) 7 | 8 | export function createRouter() { 9 | return new Router({ 10 | mode: 'history', 11 | routes: [ 12 | { path: '/', component: Home }, 13 | { path: '/search', component: Search } 14 | ] 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import { getList } from './api' 4 | 5 | Vue.use(Vuex) 6 | 7 | export function createStore() { 8 | return new Vuex.Store({ 9 | state: { 10 | result: {} 11 | }, 12 | actions: { 13 | getList({ commit }, query) { 14 | return getList(query).then(res => commit('setList', { fail: false, list: res.data })) 15 | .catch(() => commit('setList', { fail: true, list: [] })) 16 | } 17 | }, 18 | mutations: { 19 | setList(state, result) { 20 | state.result = result 21 | } 22 | } 23 | }) 24 | } -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const merge = require('lodash.merge') 2 | const nodeExternals = require('webpack-node-externals') 3 | const VueSSRClientPlugin = require('vue-server-renderer/client-plugin') 4 | const VueSSRServerPlugin = require('vue-server-renderer/server-plugin') 5 | 6 | const TARGET_NODE = process.env.WEBPACK_TARGET === 'node' 7 | 8 | const serverConfig = { 9 | entry: './src/entry.server.js', 10 | target: 'node', 11 | devtool: 'source-map', 12 | output: { 13 | libraryTarget: 'commonjs2' 14 | }, 15 | optimization: { 16 | splitChunks: false 17 | }, 18 | externals: nodeExternals({ 19 | whitelist: /\.css$/ 20 | }), 21 | plugins: [ 22 | new VueSSRServerPlugin() 23 | ] 24 | } 25 | 26 | const clientConfig = { 27 | entry: './src/entry.client.js', 28 | optimization: { 29 | splitChunks: { 30 | minChunks: Infinity 31 | } 32 | }, 33 | plugins: [ 34 | new VueSSRClientPlugin() 35 | ] 36 | } 37 | 38 | module.exports = { 39 | configureWebpack: () => TARGET_NODE ? serverConfig : clientConfig, 40 | chainWebpack: config => config.module.rule('vue').use('vue-loader') 41 | .tap(options => merge(options, { optimizeSSR: false })), 42 | devServer: { proxy: 'http://localhost:8081' } 43 | } 44 | --------------------------------------------------------------------------------