├── .gitignore ├── nodemon.json ├── src ├── static │ ├── favicon.ico │ ├── loading.png │ └── reset.css ├── components │ ├── index.js │ ├── ImgLazyLoad │ │ ├── index.less │ │ └── index.js │ ├── Loading │ │ ├── index.js │ │ └── index.less │ ├── Tab │ │ ├── index.less │ │ └── index.js │ └── LoadMore │ │ └── index.js ├── index.js ├── api │ ├── index.js │ └── http.js ├── page │ ├── cart │ │ └── index.js │ ├── mine │ │ └── index.js │ ├── sort │ │ └── index.js │ ├── index.js │ ├── topic │ │ ├── index.less │ │ └── index.js │ └── index.less ├── .nsp │ └── router.js ├── styles │ └── index.less └── container │ └── index.js ├── .nsp.js ├── bin ├── index.js └── nodeRuntime.js ├── service ├── middleware_app │ ├── bodyParser.js │ ├── views.js │ ├── router.js │ ├── staticCache.js │ └── log.js ├── utils │ ├── convert.js │ ├── Http.js │ ├── Log.js │ ├── dev.js │ └── Help.js ├── index.js ├── middleware │ ├── nspRender.js │ ├── koa-webpack-hot-middleware.js │ └── koa-webpack-dev-middleware.js ├── controller │ ├── Api.js │ └── View.js ├── views │ └── index.ejs ├── decorator │ └── index.js └── app.js ├── config ├── development.js ├── production.js └── index.js ├── jsconfig.json ├── pm2.json ├── postcss.config.js ├── webpack ├── webpack.config.dev.js ├── webpack.config.prod.js └── base.js ├── lib ├── inject.js ├── client.js ├── server.js └── analyze.js ├── .babelrc ├── LICENSE ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | build 4 | package-lock.json 5 | logs 6 | . -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["./lib", "./service", ".nsp.js", "./src/.nsp"] 3 | } 4 | -------------------------------------------------------------------------------- /src/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luoguoxiong/react-mpa-ssr/HEAD/src/static/favicon.ico -------------------------------------------------------------------------------- /src/static/loading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luoguoxiong/react-mpa-ssr/HEAD/src/static/loading.png -------------------------------------------------------------------------------- /.nsp.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | pageIgnore: [], // 忽略page的文件夹或者文件 3 | cdnPrefix: "" //生产环境cdn地址 4 | }; 5 | -------------------------------------------------------------------------------- /bin/index.js: -------------------------------------------------------------------------------- 1 | require('./nodeRuntime.js')() 2 | require('@babel/register')() 3 | require('../service') 4 | 5 | -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | import ImgLazyLoad from "./ImgLazyLoad"; 2 | import Tab from "./Tab"; 3 | export { ImgLazyLoad, Tab }; 4 | -------------------------------------------------------------------------------- /service/middleware_app/bodyParser.js: -------------------------------------------------------------------------------- 1 | import bodyParser from 'koa-bodyparser' 2 | export const addBodyParser = app => { 3 | app.use(bodyParser()) 4 | } 5 | -------------------------------------------------------------------------------- /src/components/ImgLazyLoad/index.less: -------------------------------------------------------------------------------- 1 | .imgLazyload.loading { 2 | opacity: 0.7; 3 | } 4 | .imgLazyload.loadEnd { 5 | opacity: 1; 6 | transition: all 0.5s; 7 | } 8 | -------------------------------------------------------------------------------- /config/development.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | export default { 3 | api: {}, 4 | port: 8082, 5 | env: "development", 6 | logDir: path.join(__dirname, "../logs") 7 | }; 8 | -------------------------------------------------------------------------------- /config/production.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | export default { 3 | api: {}, 4 | port: 8082, 5 | env: "production", 6 | logDir: path.join(__dirname, "../logs") 7 | }; 8 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | const fileName = 2 | "NODE_ENV" in process.env && process.env.NODE_ENV.includes("production") 3 | ? "production" 4 | : "development"; 5 | module.exports = require(`./${fileName}.js`); 6 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "emitDecoratorMetadata": true, 4 | "experimentalDecorators": true, 5 | "baseUrl": ".", 6 | "paths": { 7 | "@*": ["./*"] 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import "react-app-polyfill/ie9"; 2 | import React from "react"; 3 | import { Client } from "@lib/client"; 4 | import App from "@src/.nsp/router"; 5 | import "@src/styles/index.less"; 6 | new Client(()).render(); 7 | -------------------------------------------------------------------------------- /src/api/index.js: -------------------------------------------------------------------------------- 1 | import HttpUtils from "./http"; 2 | class Https { 3 | postLogin = parmas => HttpUtils.post("/auth/loginByMobile", parmas); 4 | 5 | getTopicList = params => HttpUtils.get("/topic/list", params); 6 | } 7 | export default new Https(); 8 | -------------------------------------------------------------------------------- /service/middleware_app/views.js: -------------------------------------------------------------------------------- 1 | import views from 'koa-views' 2 | import { resolve } from 'path' 3 | export const addViews = app => { 4 | app.use( 5 | views(resolve(__dirname, '../views'), { 6 | extension: 'ejs' 7 | }) 8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /service/middleware_app/router.js: -------------------------------------------------------------------------------- 1 | import { Route } from '../decorator' 2 | import { resolve } from 'path' 3 | export const addRouter = app => { 4 | const routesPath = resolve(__dirname, '../controller') 5 | const instance = new Route(app, routesPath) 6 | instance.init() 7 | } 8 | -------------------------------------------------------------------------------- /service/middleware_app/staticCache.js: -------------------------------------------------------------------------------- 1 | import staticCache from 'koa-static-cache' 2 | import { resolve } from 'path' 3 | export const addStaticCache = app => { 4 | app.use( 5 | staticCache(resolve(__dirname, '../../build'), { 6 | maxAge: 60 * 60 * 24 * 30, 7 | gzip: true 8 | }) 9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /bin/nodeRuntime.js: -------------------------------------------------------------------------------- 1 | const nodeRuntime = () => { 2 | var extensions = [".css", ".scss", ".less", ".png", ".jpg", ".gif"]; //服务端渲染不加载的文件类型 3 | for (let i = 0, len = extensions.length; i < len; i++) { 4 | require.extensions[extensions[i]] = function() { 5 | return false; 6 | }; 7 | } 8 | }; 9 | module.exports = nodeRuntime; 10 | -------------------------------------------------------------------------------- /service/utils/convert.js: -------------------------------------------------------------------------------- 1 | import R from 'ramda' 2 | const changeToArr = R.unless(R.is(Array), R.of) 3 | export default middleware => { 4 | return (target, key, descriptor) => { 5 | target[key] = R.compose( 6 | R.concat(changeToArr(middleware)), 7 | changeToArr 8 | )(target[key]) 9 | return descriptor 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/components/Loading/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./index.less"; 3 | const Loading = () => { 4 | return ( 5 |
6 | 7 | 8 | 9 | 10 | 11 |
12 | ); 13 | }; 14 | export default Loading; 15 | -------------------------------------------------------------------------------- /src/page/cart/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { setInitModel } from "@lib/inject"; 3 | import { Tab } from "@src/components"; 4 | 5 | @setInitModel 6 | class Cart extends React.Component { 7 | render() { 8 | return ( 9 |
10 | 购物车页 11 | 12 |
13 | ); 14 | } 15 | } 16 | export default Cart; 17 | -------------------------------------------------------------------------------- /src/page/mine/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { setInitModel } from "@lib/inject"; 3 | import { Tab } from "@src/components"; 4 | 5 | @setInitModel 6 | class Mine extends React.Component { 7 | render() { 8 | return ( 9 |
10 | 我的页 11 | 12 |
13 | ); 14 | } 15 | } 16 | export default Mine; 17 | -------------------------------------------------------------------------------- /src/page/sort/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { setInitModel } from "@lib/inject"; 3 | import { Tab } from "@src/components"; 4 | 5 | @setInitModel 6 | class Sort extends React.Component { 7 | render() { 8 | return ( 9 |
10 | 分类页 11 | 12 |
13 | ); 14 | } 15 | } 16 | export default Sort; 17 | -------------------------------------------------------------------------------- /pm2.json: -------------------------------------------------------------------------------- 1 | { 2 | "apps": [ 3 | { 4 | "name": "reactSSr", 5 | "script": "bin/index.js", 6 | "exec_mode": "fork", 7 | "instances": 1, 8 | "max_memory_restart": "1G", 9 | "autorestart": true, 10 | "node_args": [], 11 | "watch": true, 12 | "args": [], 13 | "env": { 14 | "NODE_ENV": "production" 15 | } 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /service/index.js: -------------------------------------------------------------------------------- 1 | import App from "./app"; 2 | import config from "../config"; 3 | const { port, env } = config; 4 | global.config = config; 5 | let middlewares = ["bodyParser", "log", "views", "staticCache", "router"]; 6 | try { 7 | const Server = new App(middlewares, port); 8 | if (env === "development") { 9 | Server.runDev(); 10 | } else { 11 | Server.runPro(); 12 | } 13 | } catch (e) { 14 | console.log(e); 15 | } 16 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {}, 4 | "postcss-px-to-viewport": { 5 | viewportWidth: 750, // 视窗的宽度,对应的是我们设计稿的宽度,一般是750 6 | unitPrecision: 3, // 指定`px`转换为视窗单位值的小数位数 7 | viewportUnit: "vw", //指定需要转换成的视窗单位,建议使用vw 8 | selectorBlackList: [".ignore"], // 指定不转换为视窗单位的类,可以自定义,可以无限添加,建议定义一至两个通用的类名 9 | minPixelValue: 1, // 小于或等于`1px`不转换为视窗单位,你也可以设置为你想要的值 10 | mediaQuery: false // 允许在媒体查询中转换`px` 11 | } 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /webpack/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | const webpack = require("webpack"); 2 | 3 | const { cloneDeep } = require("lodash"); 4 | 5 | const baseConfig = require("./base"); 6 | 7 | const devConfig = cloneDeep(baseConfig); 8 | 9 | devConfig.entry.assets.push( 10 | "webpack-hot-middleware/client?path=/__webpack_hmr&timeout=0&reload=true&quiet=true" 11 | ); 12 | 13 | devConfig.mode = "development"; 14 | 15 | devConfig.plugins.push(new webpack.HotModuleReplacementPlugin()); 16 | 17 | module.exports = devConfig; 18 | -------------------------------------------------------------------------------- /service/middleware/nspRender.js: -------------------------------------------------------------------------------- 1 | import Server from "@lib/server"; 2 | const nspRender = ({ title = "app" }) => { 3 | return async (ctx, next) => { 4 | await next(); 5 | let { htmlString, scripts, styles } = new Server().getSsrString( 6 | ctx.path, 7 | ctx.initModel 8 | ); 9 | await ctx.render("index", { 10 | title: ctx.title ? ctx.title : title, 11 | scripts, 12 | styles, 13 | initModel: ctx.initModel || {}, 14 | html: htmlString 15 | }); 16 | }; 17 | }; 18 | export default nspRender; 19 | -------------------------------------------------------------------------------- /service/middleware/koa-webpack-hot-middleware.js: -------------------------------------------------------------------------------- 1 | import webpackHotMiddleware from "webpack-hot-middleware"; 2 | 3 | import stream from "stream"; 4 | 5 | export default complimer => { 6 | const hotMiddleware = webpackHotMiddleware(complimer, { 7 | heartbeat: 1000 8 | }); 9 | const koaWebpackHotMiddleware = async (ctx, next) => { 10 | ctx.body = new stream.PassThrough(); 11 | await hotMiddleware(ctx.req, ctx.res, next); 12 | }; 13 | koaWebpackHotMiddleware.hotMiddleware = hotMiddleware; 14 | return koaWebpackHotMiddleware; 15 | }; 16 | -------------------------------------------------------------------------------- /lib/inject.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | const InjectContext = React.createContext(); 3 | export class Inject extends React.Component { 4 | render() { 5 | return ( 6 | 7 | {this.props.children} 8 | 9 | ); 10 | } 11 | } 12 | 13 | export const setInitModel = Component => 14 | class extends React.Component { 15 | static contextType = InjectContext; 16 | render() { 17 | return ; 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /service/utils/Http.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | class Http { 3 | constructor({ timeout = 10000, baseURL = "" }) { 4 | this.Server = axios.create({ 5 | baseURL, 6 | timeout, 7 | withCredentials: true 8 | }); 9 | } 10 | 11 | async httpRequest(url, params, method = "get") { 12 | const obj = 13 | method === "get" 14 | ? { params } 15 | : { 16 | ...params 17 | }; 18 | return (await this.Server[method](url, obj)).data; 19 | } 20 | // 添加其他请求(文件上传....) todo 21 | } 22 | module.exports = Http; 23 | -------------------------------------------------------------------------------- /service/controller/Api.js: -------------------------------------------------------------------------------- 1 | import { Controller, RequestMapping } from "../decorator"; 2 | import AxiosHttp from "../utils/Http"; 3 | const Axios = new AxiosHttp({ 4 | timeout: 10000, 5 | baseURL: "http://39.108.84.221:8888/api", 6 | }); 7 | 8 | @Controller 9 | class Api { 10 | @RequestMapping({ method: "get", url: "/topic/list" }) 11 | async topicList(ctx) { 12 | const { page, size } = ctx.query; 13 | const data = await Axios.httpRequest(`/topic/list`, { 14 | page, 15 | size, 16 | }); 17 | ctx.body = { ...data }; 18 | } 19 | } 20 | export default Api; 21 | -------------------------------------------------------------------------------- /service/middleware/koa-webpack-dev-middleware.js: -------------------------------------------------------------------------------- 1 | import webpackDevMiddleware from "webpack-dev-middleware"; 2 | 3 | export default (compiler, options) => { 4 | const devMiddleware = webpackDevMiddleware(compiler, options); 5 | const koaMiddleware = (ctx, next) => { 6 | const res = {}; 7 | res.end = data => { 8 | ctx.body = data; 9 | }; 10 | res.setHeader = (name, value) => { 11 | ctx.set(name, value); 12 | }; 13 | return devMiddleware(ctx.req, res, next); 14 | }; 15 | Object.keys(devMiddleware).forEach(item => { 16 | koaMiddleware[item] = devMiddleware[item]; 17 | }); 18 | return koaMiddleware; 19 | }; 20 | -------------------------------------------------------------------------------- /src/components/Tab/index.less: -------------------------------------------------------------------------------- 1 | #tabCom { 2 | height: 110px; 3 | .tabContent { 4 | position: fixed; 5 | bottom: 0; 6 | left: 0; 7 | width: 100%; 8 | height: 110px; 9 | background: white; 10 | box-shadow: 0 -3px 10px 0 rgba(0, 0, 0, 0.2); 11 | z-index: 2; 12 | display: flex; 13 | color: #333; 14 | & > .active { 15 | color: #2196f3; 16 | } 17 | & > div { 18 | flex: 1; 19 | height: 100%; 20 | .tabIcon { 21 | height: 70px; 22 | display: flex; 23 | justify-content: center; 24 | align-items: center; 25 | i { 26 | font-size: 40px; 27 | } 28 | } 29 | .tabName { 30 | font-size: 24px; 31 | line-height: 40px; 32 | text-align: center; 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "browsers": ["IE >= 9"] 8 | } 9 | // "corejs": "3", 10 | // "useBuiltIns": "usage" 11 | } 12 | ], 13 | "@babel/preset-react" 14 | ], 15 | "plugins": [ 16 | [ 17 | "module-resolver", 18 | { 19 | "alias": { 20 | "@src": "./src", 21 | "@lib": "./lib", 22 | "@build": "./build", 23 | "@service": "./service", 24 | "@config": "./config" 25 | } 26 | } 27 | ], 28 | [ 29 | "@babel/plugin-proposal-decorators", 30 | { 31 | "legacy": true 32 | } 33 | ], 34 | "nsploadable/babel", 35 | "@babel/plugin-transform-runtime", 36 | "@babel/plugin-proposal-class-properties" 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /src/page/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { setInitModel } from "@lib/inject"; 3 | import { Tab } from "@src/components"; 4 | import { Banner, Channel, Brand, News, Hots } from "@src/container"; 5 | import "./index.less"; 6 | @setInitModel 7 | export default class Home extends React.PureComponent { 8 | render() { 9 | const { errno, data } = this.props; 10 | if (errno === 0) { 11 | const { banner, channel, brandList, newGoodsList, hotGoodsList } = data; 12 | return ( 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | ); 22 | } else { 23 | return null; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/components/Loading/index.less: -------------------------------------------------------------------------------- 1 | #loadingOp { 2 | width: 100%; 3 | height: 100px; 4 | background: white; 5 | display: flex; 6 | justify-content: center; 7 | align-items: center; 8 | } 9 | #loadingOp span { 10 | display: inline-block; 11 | width: 8px; 12 | border-radius: 8px; 13 | background: lightgreen; 14 | margin-left: 15px; 15 | animation: load 1s ease infinite; 16 | } 17 | @keyframes load { 18 | 0%, 19 | 100% { 20 | height: 20px; 21 | background: lightgreen; 22 | } 23 | 50% { 24 | height: 35px; 25 | margin: -15px 0; 26 | background: lightblue; 27 | } 28 | } 29 | #loadingOp span:nth-child(2) { 30 | animation-delay: 0.2s; 31 | } 32 | #loadingOp span:nth-child(3) { 33 | animation-delay: 0.4s; 34 | } 35 | #loadingOp span:nth-child(4) { 36 | animation-delay: 0.6s; 37 | } 38 | #loadingOp span:nth-child(5) { 39 | animation-delay: 0.8s; 40 | } 41 | -------------------------------------------------------------------------------- /webpack/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | const { cloneDeep } = require("lodash"); 2 | 3 | const baseConfig = require("./base"); 4 | 5 | const BundleAnalyzerPlugin = require("webpack-bundle-analyzer") 6 | .BundleAnalyzerPlugin; 7 | 8 | const UglifyJsPlugin = require("uglifyjs-webpack-plugin"); 9 | 10 | const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin"); 11 | 12 | const { CleanWebpackPlugin } = require("clean-webpack-plugin"); 13 | 14 | const prodConfig = cloneDeep(baseConfig); 15 | 16 | prodConfig.mode = "production"; 17 | 18 | prodConfig.plugins.unshift(new CleanWebpackPlugin()); 19 | 20 | if (process.env.Analyze) { 21 | prodConfig.plugins.push(new BundleAnalyzerPlugin()); 22 | } 23 | 24 | prodConfig.optimization.minimizer.push( 25 | //压缩js 26 | new UglifyJsPlugin({ 27 | cache: true, 28 | parallel: true, 29 | sourceMap: false 30 | }), 31 | // 压缩css 32 | new OptimizeCSSAssetsPlugin({}) 33 | ); 34 | 35 | module.exports = prodConfig; 36 | -------------------------------------------------------------------------------- /src/api/http.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | const instance = axios.create({ 3 | baseURL: "", 4 | withCredentials: true // 跨域类型时是否在请求中协带cookie 5 | }); 6 | export default class HttpUtil { 7 | static get(url, params = {}) { 8 | return new Promise((resolve, reject) => { 9 | instance 10 | .get(url, { params }) 11 | .then(({ data }) => { 12 | if (data.errno === 0) { 13 | resolve(data.data); 14 | } else { 15 | resolve(data); 16 | } 17 | }) 18 | .catch(err => { 19 | reject({ err: JSON.stringify(err) }); 20 | }); 21 | }); 22 | } 23 | static post(url, params = {}) { 24 | return new Promise((resolve, reject) => { 25 | instance 26 | .post(url, { ...params }) 27 | .then(({ data }) => { 28 | resolve(data); 29 | }) 30 | .catch(err => { 31 | reject({ err: JSON.stringify(err) }); 32 | }); 33 | }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /service/utils/Log.js: -------------------------------------------------------------------------------- 1 | import log4js from "log4js"; 2 | import Help from "./Help"; 3 | class Log { 4 | static initLog(type, withDate = true) { 5 | const filename = withDate 6 | ? `${global.config.logDir}/${type}.${Help.newDateYYMMDD()}.log` 7 | : `${global.config.logDir}/${type}.log`; 8 | log4js.configure({ 9 | appenders: { 10 | logs: { 11 | type: "dateFile", 12 | filename, 13 | pattern: ".yyyy-MM-dd", 14 | alwaysIncludePattern: false 15 | } 16 | }, 17 | categories: { 18 | default: { appenders: ["logs"], level: "debug" } 19 | } 20 | }); 21 | return log4js.getLogger("SERVICELOG"); 22 | } 23 | static error(errorMsg) { 24 | Log.initLog("Error").error(errorMsg); 25 | Log.initLog("Log", false).error(errorMsg); 26 | } 27 | static info(infoMsg) { 28 | Log.initLog("Info").info(infoMsg); 29 | Log.initLog("Log", false).info(infoMsg); 30 | } 31 | } 32 | export default Log; 33 | -------------------------------------------------------------------------------- /src/components/LoadMore/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | class LoadMore extends React.Component { 4 | componentDidMount() { 5 | window.addEventListener("scroll", this.myEfficientFn); 6 | } 7 | 8 | componentWillUnmount() { 9 | window.removeEventListener("scroll", this.myEfficientFn); 10 | } 11 | 12 | debounce(func, wait) { 13 | let timeout = null; 14 | return () => { 15 | const scrollY = window.scrollY; 16 | this.props.onScroll && this.props.onScroll(scrollY); 17 | clearTimeout(timeout); 18 | timeout = setTimeout(() => { 19 | func.apply(this); 20 | }, wait); 21 | }; 22 | } 23 | 24 | myEfficientFn = this.debounce(this.scrollListener, 250); 25 | 26 | scrollListener() { 27 | const scrollY = window.scrollY; 28 | if ( 29 | scrollY + document.documentElement.clientHeight >= 30 | document.body.clientHeight - 1 && 31 | this.props.isAllow 32 | ) { 33 | this.props.onBottom(); 34 | } 35 | } 36 | render() { 37 | return this.props.children; 38 | } 39 | } 40 | 41 | export default LoadMore; 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 luoGuoXiong 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 | -------------------------------------------------------------------------------- /src/page/topic/index.less: -------------------------------------------------------------------------------- 1 | #topicPage { 2 | .topicItem { 3 | margin-top: 20px; 4 | height: 550px; 5 | padding-bottom: 20px; 6 | box-shadow: 0 0 20px 0 rgba(128, 145, 165, 0.2); 7 | background: white; 8 | img { 9 | width: 100%; 10 | height: 400px; 11 | } 12 | .topicName { 13 | padding: 10px 20px; 14 | background: white; 15 | height: 30px; 16 | text-align: center; 17 | line-height: 30px; 18 | text-align: center; 19 | overflow: hidden; 20 | text-overflow: ellipsis; 21 | white-space: nowrap; 22 | } 23 | .topicName:nth-child(2) { 24 | padding-top: 20px; 25 | font-size: 30px; 26 | } 27 | .topicName:nth-child(3) { 28 | font-size: 26px; 29 | color: grey; 30 | } 31 | .topicName:nth-child(4) { 32 | font-size: 28px; 33 | color: red; 34 | } 35 | } 36 | & > .topicItem:nth-child(1) { 37 | margin-top: 0; 38 | } 39 | .noHasAny { 40 | height: 80px; 41 | text-align: center; 42 | line-height: 80px; 43 | font-size: 30px; 44 | color: gray; 45 | background: white; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /service/views/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | <%= title %> 13 | 17 | 21 | 25 | 26 | <%- styles %> 27 | 28 | 31 | 32 |
<%- html %>
33 | <%- scripts %> 34 | 35 | 36 | -------------------------------------------------------------------------------- /service/utils/dev.js: -------------------------------------------------------------------------------- 1 | import webpack from "webpack"; 2 | 3 | import koaWebpackDevMiddleware from "../middleware/koa-webpack-dev-middleware"; 4 | 5 | import koaWebpackHotMiddleware from "../middleware/koa-webpack-hot-middleware"; 6 | 7 | const webpackClientConfig = require("../../webpack/webpack.config.dev"); 8 | 9 | export default (app, callback) => { 10 | let hmrKeyT = null; 11 | 12 | const clientCompiler = webpack(webpackClientConfig); 13 | 14 | const koaWebpackHotMiddlewareObject = koaWebpackHotMiddleware(clientCompiler); 15 | 16 | let hotMiddleware = koaWebpackHotMiddlewareObject.hotMiddleware; 17 | clientCompiler.plugin("done", () => { 18 | if (hotMiddleware && typeof hotMiddleware.publish === "function") { 19 | if (hmrKeyT) global.clearInterval(hmrKeyT); 20 | const hmrKey = new Date().getSeconds(); 21 | hmrKeyT = global.setInterval(() => { 22 | hotMiddleware.publish({ 23 | action: "bundled", 24 | hmrKey 25 | }); 26 | }, 1000); 27 | callback(); 28 | } 29 | }); 30 | 31 | app.use( 32 | koaWebpackDevMiddleware(clientCompiler, { 33 | stats: "errors-only" 34 | }) 35 | ); 36 | app.use(koaWebpackHotMiddlewareObject); 37 | }; 38 | -------------------------------------------------------------------------------- /service/utils/Help.js: -------------------------------------------------------------------------------- 1 | import R from "ramda"; 2 | const changeToArr = R.unless(R.is(Array), R.of); 3 | class Help { 4 | // 生成当前时间的YY-MM-DD的时间格式 5 | static newDateYYMMDD() { 6 | const date = new Date(); 7 | const year = date.getFullYear(); 8 | const month = date.getMonth() + 1; 9 | const day = date.getDate(); 10 | return [year, month, day] 11 | .map(number => { 12 | return number < 10 ? `0${number}` : number; 13 | }) 14 | .join("-"); 15 | } 16 | // 获取url的请求参数 17 | static getRequestParmas(url) { 18 | const theRequest = new Object(); 19 | if (url.indexOf("?") != -1) { 20 | const reqStr = url.split("?")[1].split("&"); 21 | for (var i = 0; i < reqStr.length; i++) { 22 | theRequest[reqStr[i].split("=")[0]] = reqStr[i].split("=")[1]; 23 | } 24 | } 25 | return theRequest; 26 | } 27 | 28 | /** 29 | * 中间封装成装饰器 30 | * @param {中间间} middleware 31 | */ 32 | static convert(middleware) { 33 | return (target, key, descriptor) => { 34 | target[key] = R.compose( 35 | R.concat(changeToArr(middleware)), 36 | changeToArr 37 | )(target[key]); 38 | return descriptor; 39 | }; 40 | } 41 | } 42 | export default Help; 43 | -------------------------------------------------------------------------------- /service/controller/View.js: -------------------------------------------------------------------------------- 1 | import { Controller, RequestMapping, NspRender } from "../decorator"; 2 | import AxiosHttp from "../utils/Http"; 3 | const Axios = new AxiosHttp({ 4 | timeout: 10000, 5 | baseURL: "http://39.108.84.221:8888/api", 6 | }); 7 | 8 | @Controller 9 | class View { 10 | @RequestMapping({ method: "get", url: "/" }) 11 | @NspRender({ title: "NSP严选" }) 12 | async home(ctx) { 13 | const data = await Axios.httpRequest("/"); 14 | ctx.initModel = { ...data }; 15 | } 16 | 17 | @RequestMapping({ method: "get", url: "/topic" }) 18 | @NspRender({ title: "NSP专题" }) 19 | async topic(ctx) { 20 | const data = await Axios.httpRequest("/topic/list", { 21 | page: 1, 22 | size: 6, 23 | }); 24 | ctx.initModel = { ...data }; 25 | } 26 | 27 | @RequestMapping({ method: "get", url: "/sort" }) 28 | @NspRender({ title: "NSP分类" }) 29 | async sort(ctx) { 30 | ctx.initModel = {}; 31 | } 32 | 33 | @RequestMapping({ method: "get", url: "/cart" }) 34 | @NspRender({ title: "NSP购物车" }) 35 | async cart(ctx) { 36 | ctx.initModel = {}; 37 | } 38 | 39 | @RequestMapping({ method: "get", url: "/mine" }) 40 | @NspRender({ title: "NSP用户" }) 41 | async mine(ctx) { 42 | ctx.initModel = {}; 43 | } 44 | } 45 | export default View; 46 | -------------------------------------------------------------------------------- /lib/client.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Loadable from "nsploadable"; 3 | import { hydrate, render } from "react-dom"; 4 | import { BrowserRouter } from "react-router-dom"; 5 | import { Inject } from "./inject"; 6 | 7 | export class Client { 8 | constructor(component = null) { 9 | this.component = ( 10 | 11 | {component} 12 | 13 | ); 14 | } 15 | 16 | render() { 17 | window.onload = () => { 18 | if (module.hot) { 19 | let hmrKey; 20 | const hotClient = require("webpack-hot-middleware/client"); 21 | hotClient.setOptionsAndConnect({ timeout: 2000, quiet: true }); 22 | hotClient.subscribe(e => { 23 | if (e.action === "bundled") { 24 | if (hmrKey && hmrKey !== e.hmrKey) { 25 | window.location.reload(); 26 | } else { 27 | hmrKey = e.hmrKey; 28 | } 29 | } 30 | }); 31 | } 32 | Loadable.preloadReady().then(() => { 33 | if (module.hot) { 34 | render(this.component, document.getElementById("app")); 35 | } else { 36 | hydrate(this.component, document.getElementById("app")); 37 | } 38 | }); 39 | }; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/components/Tab/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./index.less"; 3 | export default class Tab extends React.PureComponent { 4 | state = { 5 | list: [ 6 | { icon: "iconfont icon-caidaniconshouyehui", name: "首页", url: "/" }, 7 | { icon: "iconfont icon-clone", name: "专题", url: "/topic" }, 8 | { icon: "iconfont icon-sort", name: "分类", url: "/sort" }, 9 | { icon: "iconfont icon-cart", name: "购物车", url: "/cart" }, 10 | { icon: "iconfont icon-mine", name: "我的", url: "/mine" } 11 | ] 12 | }; 13 | linkto = item => { 14 | window.location.replace(item.url); 15 | }; 16 | render() { 17 | const { list } = this.state; 18 | const { active = 0 } = this.props; 19 | return ( 20 | 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/.nsp/router.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from "react"; 2 | import { Route } from "react-router-dom"; 3 | import Loadable from "nsploadable"; 4 | 5 | const Nsp_cart = Loadable({ 6 | loader: () => import(/* webpackChunkName: 'Nsp_cart' */ '../page/cart') 7 | }); 8 | 9 | const Nsp_ = Loadable({ 10 | loader: () => import(/* webpackChunkName: 'Nsp_' */ '../page/') 11 | }); 12 | 13 | const Nsp_mine = Loadable({ 14 | loader: () => import(/* webpackChunkName: 'Nsp_mine' */ '../page/mine') 15 | }); 16 | 17 | const Nsp_sort = Loadable({ 18 | loader: () => import(/* webpackChunkName: 'Nsp_sort' */ '../page/sort') 19 | }); 20 | 21 | const Nsp_topic = Loadable({ 22 | loader: () => import(/* webpackChunkName: 'Nsp_topic' */ '../page/topic') 23 | }); 24 | 25 | const routes = [ 26 | { 27 | path: "/cart", 28 | component: 29 | }, 30 | { 31 | path: "/", 32 | component: 33 | }, 34 | { 35 | path: "/mine", 36 | component: 37 | }, 38 | { 39 | path: "/sort", 40 | component: 41 | }, 42 | { 43 | path: "/topic", 44 | component: 45 | }, 46 | ] 47 | 48 | class App extends React.Component { 49 | render () { 50 | return ( 51 | 52 | {routes.map(item => ( 53 | 54 | {item.component} 55 | 56 | ))} 57 | 58 | ); 59 | } 60 | } 61 | 62 | export default App; -------------------------------------------------------------------------------- /service/middleware_app/log.js: -------------------------------------------------------------------------------- 1 | import Log from "../utils/Log"; 2 | import Help from "../utils/Help"; 3 | const logTemplate = (reqMethod, reqUrl, parmas, resBody, reqTime) => { 4 | const template = 5 | `method: ${reqMethod} ===> url: ${ 6 | reqUrl.split("?")[0] 7 | } ===> parmas: ${parmas} ===> times:${reqTime}` + 8 | ` ===> result: ${resBody.slice(0, 100)}`; 9 | return template; 10 | }; 11 | export const addLog = app => { 12 | app.use(async (ctx, next) => { 13 | const startTime = new Date().getTime(); 14 | const reqMethod = ctx.method; 15 | const reqUrl = ctx.request.url; 16 | const parmas = 17 | reqMethod === "GET" 18 | ? JSON.stringify(Help.getRequestParmas(reqUrl)) 19 | : JSON.stringify(ctx.request.body); 20 | try { 21 | await next(); 22 | const endTime = new Date().getTime(); 23 | const reqTime = endTime - startTime + "ms"; 24 | const { 25 | response: { status, message } 26 | } = ctx; 27 | const resBody = JSON.stringify(status === 200 ? ctx.body : message); 28 | const info = logTemplate(reqMethod, reqUrl, parmas, resBody, reqTime); 29 | status === 200 ? Log.info(info) : Log.error(info); 30 | } catch (e) { 31 | const endTime = new Date().getTime(); 32 | const reqTime = endTime - startTime + "ms"; 33 | const resBody = e.message; 34 | const error = logTemplate(reqMethod, reqUrl, parmas, resBody, reqTime); 35 | Log.error(error); 36 | ctx.body = e.message; 37 | } 38 | }); 39 | }; 40 | -------------------------------------------------------------------------------- /src/styles/index.less: -------------------------------------------------------------------------------- 1 | #content { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | position: fixed; 6 | top: 0; 7 | bottom: 110px; 8 | width: 100%; 9 | color: #2196f3; 10 | } 11 | 12 | .onePx_bottom { 13 | position: relative; 14 | &:after { 15 | content: ""; 16 | width: 200%; 17 | background: #ededed; 18 | height: 1px; 19 | position: absolute; 20 | bottom: 0; 21 | left: 0; 22 | transform: scale(0.5); 23 | transform-origin: 0; 24 | } 25 | } 26 | .onePx_top { 27 | position: relative; 28 | &:after { 29 | content: ""; 30 | width: 200%; 31 | background: #ededed; 32 | height: 1px; 33 | position: absolute; 34 | top: 0; 35 | left: 0; 36 | transform: scale(0.5); 37 | transform-origin: 0; 38 | } 39 | } 40 | .onePx_left { 41 | position: relative; 42 | &:after { 43 | content: ""; 44 | width: 1px; 45 | background: #ededed; 46 | height: 200%; 47 | position: absolute; 48 | top: 0; 49 | left: 0; 50 | transform: scale(0.5); 51 | transform-origin: 0; 52 | } 53 | } 54 | .onePx_right { 55 | position: relative; 56 | &:after { 57 | content: ""; 58 | width: 1px; 59 | background: #ededed; 60 | height: 200%; 61 | position: absolute; 62 | top: 0; 63 | right: 0; 64 | transform: scale(0.5); 65 | transform-origin: 0; 66 | } 67 | } 68 | .onePx_border { 69 | position: relative; 70 | &:after { 71 | content: ""; 72 | width: 200%; 73 | border: 1px solid #ededed; 74 | height: 200%; 75 | position: absolute; 76 | top: 0; 77 | left: 0; 78 | transform: scale(0.5); 79 | transform-origin: 0 0; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /service/decorator/index.js: -------------------------------------------------------------------------------- 1 | import { resolve } from "path"; 2 | import KoaRouter from "koa-router"; 3 | import glob from "glob"; 4 | import R from "ramda"; 5 | import nspRender from "../middleware/nspRender"; 6 | import convert from "../utils/convert"; 7 | 8 | const pathPrefix = Symbol("pathPrefix"); 9 | const routeMap = []; 10 | 11 | const resolvePath = R.unless(R.startsWith("/"), R.curryN(2, R.concat)("/")); 12 | const changeToArr = R.unless(R.is(Array), R.of); 13 | 14 | export class Route { 15 | constructor(app, routesPath) { 16 | this.app = app; 17 | this.router = new KoaRouter(); 18 | this.routesPath = routesPath; 19 | } 20 | 21 | init = () => { 22 | const { app, router, routesPath } = this; 23 | glob.sync(resolve(routesPath, "./*.js")).forEach(require); 24 | R.forEach(({ target, withTarget, method, path, callback }) => { 25 | if (withTarget) { 26 | const prefix = resolvePath(target[pathPrefix]); 27 | router[method](prefix + path, ...callback); 28 | } else { 29 | router[method](path, ...callback); 30 | } 31 | })(routeMap); 32 | 33 | app.use(router.routes()); 34 | app.use(router.allowedMethods()); 35 | }; 36 | } 37 | 38 | export const RequestMapping = (requestmapping = { method: "get", url: "" }) => ( 39 | target, 40 | key, 41 | descriptor 42 | ) => { 43 | routeMap.push({ 44 | target: target, 45 | method: requestmapping.method, 46 | withTarget: requestmapping.url ? false : true, 47 | path: requestmapping.url ? requestmapping.url : `/${descriptor.value.name}`, 48 | callback: changeToArr(target[key]) 49 | }); 50 | return descriptor; 51 | }; 52 | 53 | export const Controller = target => 54 | (target.prototype[pathPrefix] = target.name); 55 | 56 | export const NspRender = parmas => convert(nspRender(parmas)); 57 | -------------------------------------------------------------------------------- /src/static/reset.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, 7 | body, 8 | div, 9 | span, 10 | applet, 11 | object, 12 | iframe, 13 | h1, 14 | h2, 15 | h3, 16 | h4, 17 | h5, 18 | h6, 19 | p, 20 | blockquote, 21 | pre, 22 | a, 23 | abbr, 24 | acronym, 25 | address, 26 | big, 27 | cite, 28 | code, 29 | del, 30 | dfn, 31 | em, 32 | img, 33 | ins, 34 | kbd, 35 | q, 36 | s, 37 | samp, 38 | small, 39 | strike, 40 | strong, 41 | sub, 42 | sup, 43 | tt, 44 | var, 45 | b, 46 | u, 47 | i, 48 | center, 49 | dl, 50 | dt, 51 | dd, 52 | ol, 53 | ul, 54 | li, 55 | fieldset, 56 | form, 57 | label, 58 | legend, 59 | table, 60 | caption, 61 | tbody, 62 | tfoot, 63 | thead, 64 | tr, 65 | th, 66 | td, 67 | article, 68 | aside, 69 | canvas, 70 | details, 71 | embed, 72 | figure, 73 | figcaption, 74 | footer, 75 | header, 76 | hgroup, 77 | menu, 78 | nav, 79 | output, 80 | ruby, 81 | section, 82 | summary, 83 | time, 84 | mark, 85 | audio, 86 | video { 87 | margin: 0; 88 | padding: 0; 89 | border: 0; 90 | font-size: 100%; 91 | font: inherit; 92 | vertical-align: baseline; 93 | font-weight: normal; 94 | } 95 | /* HTML5 display-role reset for older browsers */ 96 | article, 97 | aside, 98 | details, 99 | figcaption, 100 | figure, 101 | footer, 102 | header, 103 | hgroup, 104 | menu, 105 | nav, 106 | section { 107 | display: block; 108 | } 109 | body { 110 | line-height: 1; 111 | } 112 | ol, 113 | ul { 114 | list-style: none; 115 | } 116 | blockquote, 117 | q { 118 | quotes: none; 119 | } 120 | blockquote:before, 121 | blockquote:after, 122 | q:before, 123 | q:after { 124 | content: ""; 125 | content: none; 126 | } 127 | table { 128 | border-collapse: collapse; 129 | border-spacing: 0; 130 | } 131 | -------------------------------------------------------------------------------- /src/components/ImgLazyLoad/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import "./index.less"; 3 | class ImgLazyLoad extends Component { 4 | constructor(props) { 5 | super(props); 6 | this.state = { 7 | isLoad: false, 8 | isLoading: false 9 | }; 10 | this.handler = this.handler.bind(this); 11 | } 12 | componentDidMount() { 13 | this.handler(); 14 | window.addEventListener("scroll", this.handler); 15 | } 16 | handler() { 17 | if (!this.state.isLoading) { 18 | const { offSetTop, realUrl } = this.props; 19 | const visibleBottom = 20 | window.scrollY + document.documentElement.clientHeight - offSetTop; 21 | const imgTop = this.refs.imgLazyLoad.getBoundingClientRect().top; 22 | if (imgTop < visibleBottom) { 23 | let imgObj = new Image(); 24 | imgObj.src = realUrl; 25 | this.setState({ isLoading: true }); 26 | new Promise((resolve, reject) => { 27 | imgObj.onload = function() { 28 | resolve(imgObj); 29 | }; 30 | }).then(imgObj => { 31 | this.setState({ isLoad: true }); 32 | }); 33 | } 34 | } else { 35 | window.removeEventListener("scroll", this.handler); 36 | } 37 | } 38 | componentWillUnmount() { 39 | window.removeEventListener("scroll", this.handler); 40 | } 41 | render() { 42 | const { isLoad } = this.state; 43 | const { realUrl, initUrl } = this.props; 44 | const imgSrc = isLoad ? realUrl : initUrl; 45 | return ( 46 | imgLazyLoad 52 | ); 53 | } 54 | } 55 | ImgLazyLoad.defaultProps = { 56 | offSetTop: 0, 57 | initUrl: "/static/loading.png" 58 | }; 59 | export default ImgLazyLoad; 60 | -------------------------------------------------------------------------------- /src/page/topic/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Tab, ImgLazyLoad } from "@src/components"; 3 | import LoadMore from "@src/components/LoadMore"; 4 | import { setInitModel } from "@lib/inject"; 5 | import http from "@src/api"; 6 | import "./index.less"; 7 | @setInitModel 8 | export default class User extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | let { errno } = props; 12 | if (errno === 0) { 13 | this.state = { 14 | errno, 15 | ...props.data 16 | }; 17 | } else { 18 | this.state = { 19 | errno 20 | }; 21 | } 22 | } 23 | onBottom = async () => { 24 | const { currentPage, data } = this.state; 25 | const list = await http.getTopicList({ page: currentPage + 1, size: 6 }); 26 | data.push(...list.data); 27 | this.setState({ data, currentPage: currentPage + 1 }); 28 | }; 29 | 30 | render() { 31 | if (this.state.errno === 0) { 32 | const { data, count } = this.state; 33 | return ( 34 |
35 | data.length}> 36 | {data.map(item => ( 37 |
38 | 42 |
{item.title}
43 |
{item.subtitle}
44 |
{item.price_info}元起
45 |
46 | ))} 47 | {count == data.length && ( 48 |
沒有更多~
49 | )} 50 |
51 | 52 |
53 | ); 54 | } else { 55 | return null; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/server.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { renderToString } from "react-dom/server"; 3 | import Loadable from "nsploadable"; 4 | import { StaticRouter } from "react-router-dom"; 5 | import { getBundles } from "nsploadable/webpack"; 6 | import manifest from "@build/manifest.json"; 7 | import loadJson from "@build/loadable.json"; 8 | import App from "@src/.nsp/router"; 9 | import { Inject } from "./inject"; 10 | class Server { 11 | // 获取客户端异步的文件 12 | static getAsyncOptions = (modules, json) => { 13 | let bundles = getBundles(json, modules); 14 | let asyncJsLinks = bundles 15 | .filter(bundle => bundle.file.endsWith(".js")) 16 | .map(item => `${item.publicPath}`); 17 | let asyncCssLinks = bundles 18 | .filter(bundle => bundle.file.endsWith(".css")) 19 | .map(item => `${item.publicPath}`); 20 | return { 21 | asyncJsLinks, 22 | asyncCssLinks 23 | }; 24 | }; 25 | 26 | // 获取客户端公共的文件 27 | static getPublicOptions = () => { 28 | const jsLinks = []; 29 | const cssLinks = []; 30 | for (let key in manifest) { 31 | if (key.includes("assets")) { 32 | key.includes("css") && cssLinks.push(manifest[key]); 33 | key.includes("js") && jsLinks.push(manifest[key]); 34 | } 35 | } 36 | return { jsLinks, cssLinks }; 37 | }; 38 | 39 | // 客户端文件转link和script字符串 40 | static getOptionsString = (modules, json) => { 41 | const { asyncCssLinks, asyncJsLinks } = Server.getAsyncOptions( 42 | modules, 43 | json 44 | ); 45 | const { jsLinks, cssLinks } = Server.getPublicOptions(); 46 | const js = [...jsLinks, ...asyncJsLinks]; 47 | const css = [...cssLinks, ...asyncCssLinks]; 48 | const scripts = js 49 | .map(script => ``) 50 | .join(""); 51 | const styles = css 52 | .map(style => ``) 53 | .join(""); 54 | return { scripts, styles }; 55 | }; 56 | 57 | // 服务端渲染dom树 58 | static getHtmlString(reqUrl, initModel, modules = []) { 59 | return renderToString( 60 | 61 | 62 | modules.push(moduleName)}> 63 | 64 | 65 | 66 | 67 | ); 68 | } 69 | 70 | getSsrString = (reqUrl, initModel = {}, modules = []) => { 71 | const htmlString = Server.getHtmlString(reqUrl, { initModel }, modules); 72 | const { scripts, styles } = Server.getOptionsString(modules, loadJson); 73 | return { htmlString, scripts, styles }; 74 | }; 75 | } 76 | export default Server; 77 | -------------------------------------------------------------------------------- /service/app.js: -------------------------------------------------------------------------------- 1 | import Koa from "koa"; 2 | import Loadable from "nsploadable"; 3 | import R from "ramda"; 4 | import { join, resolve } from "path"; 5 | import chalk from "chalk"; 6 | import chokidar from "chokidar"; 7 | import RouterAnalyze from "@lib/analyze"; 8 | import dev from "./utils/dev"; 9 | const entry = resolve(__dirname, "../src/page"); 10 | const output = resolve(__dirname, "../src/.nsp/router.js"); 11 | import webpack from "webpack"; 12 | import webpackConfig from "../webpack/webpack.config.prod"; 13 | class App { 14 | constructor(middlewares, port) { 15 | this.app = new Koa(); 16 | this.isListen = false; 17 | this.middlewares = middlewares; 18 | this.port = port; 19 | } 20 | 21 | useMiddleware() { 22 | const joinPathName = moduleName => 23 | join(__dirname, `./middleware_app/${moduleName}`); 24 | 25 | const requirePath = pathName => require(pathName); 26 | 27 | const useMiddleware = R.forEachObjIndexed(middlewaresUseByApp => 28 | middlewaresUseByApp(this.app) 29 | ); 30 | 31 | const Rcompose = R.compose(useMiddleware, requirePath, joinPathName); 32 | R.map(Rcompose)(this.middlewares); 33 | } 34 | 35 | createHttpServer() { 36 | this.useMiddleware(); 37 | Loadable.preloadAll().then(() => { 38 | this.app.listen(this.port, err => { 39 | console.log( 40 | chalk.green( 41 | `Nsp is Listening on port ${this.port}. Open up http://localhost:${this.port}/ in your browser.\n` 42 | ) 43 | ); 44 | this.isListen = true; 45 | }); 46 | }); 47 | } 48 | 49 | runDevTodo() { 50 | new RouterAnalyze(entry, output, () => { 51 | if (!this.isListen) { 52 | dev(this.app, () => { 53 | !this.isListen && this.createHttpServer(); 54 | }); 55 | } 56 | }); 57 | } 58 | 59 | runDev() { 60 | const watcher = chokidar.watch(join(__dirname, "../src/page"), { 61 | ignored: /(^|[\/\\])\../, 62 | persistent: true 63 | }); 64 | watcher.on("all", event => { 65 | if ( 66 | this.isListen && 67 | (event.includes("add") || 68 | event.includes("unlink") || 69 | event.includes("addDir") || 70 | event.includes("unlinkDir")) 71 | ) { 72 | this.runDevTodo(); 73 | } 74 | }); 75 | watcher.on("ready", () => { 76 | console.log(chalk.green("√ Initial watcher complete. Ready for changes")); 77 | this.runDevTodo(); 78 | }); 79 | } 80 | 81 | runPro() { 82 | new RouterAnalyze(entry, output, () => { 83 | webpack(webpackConfig, () => { 84 | this.createHttpServer(); 85 | }); 86 | }); 87 | } 88 | } 89 | export default App; 90 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nsp", 3 | "version": "1.0.0", 4 | "description": "nsp是koa+react服务端渲染框架", 5 | "scripts": { 6 | "dev": "cross-env NODE_ENV=development nodemon node bin/index.js", 7 | "build": "cross-env NODE_ENV=production node bin/index.js", 8 | "analyze": "cross-env NODE_ENV=production Analyze=true webpack --config webpack/webpack.config.prod.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/Peroluo/NSP.git" 13 | }, 14 | "keywords": [ 15 | "react", 16 | "nsp" 17 | ], 18 | "author": "luoguoxiong", 19 | "license": "ISC", 20 | "bugs": { 21 | "url": "https://github.com/Peroluo/NSP/issues" 22 | }, 23 | "homepage": "https://github.com/Peroluo/NSP#readme", 24 | "dependencies": { 25 | "antd-mobile": "^2.3.1", 26 | "axios": "^0.19.0", 27 | "nsploadable": "^0.0.2", 28 | "react": "^16.11.0", 29 | "react-dom": "^16.11.0", 30 | "react-router-dom": "^5.1.2" 31 | }, 32 | "devDependencies": { 33 | "@babel/cli": "^7.7.0", 34 | "@babel/core": "^7.7.2", 35 | "@babel/node": "^7.7.0", 36 | "@babel/plugin-proposal-class-properties": "^7.7.0", 37 | "@babel/plugin-proposal-decorators": "^7.7.0", 38 | "@babel/plugin-syntax-dynamic-import": "^7.2.0", 39 | "@babel/plugin-transform-runtime": "^7.6.2", 40 | "@babel/preset-env": "^7.7.1", 41 | "@babel/preset-react": "^7.7.0", 42 | "autoprefixer": "^9.7.1", 43 | "babel-loader": "^8.0.6", 44 | "babel-plugin-module-resolver": "^3.2.0", 45 | "chokidar": "^3.3.0", 46 | "clean-webpack-plugin": "^3.0.0", 47 | "copy-webpack-plugin": "^5.0.5", 48 | "core-js": "^3.4.1", 49 | "cross-env": "^6.0.3", 50 | "css-loader": "^3.2.0", 51 | "friendly-errors-webpack-plugin": "^1.7.0", 52 | "glob": "^7.1.5", 53 | "happypack": "^5.0.1", 54 | "koa": "^2.10.0", 55 | "koa-bodyparser": "^4.2.1", 56 | "koa-router": "^7.4.0", 57 | "koa-static-cache": "^5.1.2", 58 | "koa-views": "^6.2.1", 59 | "less": "^3.10.3", 60 | "less-loader": "^5.0.0", 61 | "lodash": "^4.17.15", 62 | "log4js": "^6.1.0", 63 | "mini-css-extract-plugin": "^0.8.0", 64 | "nodemon": "^1.19.4", 65 | "npm-run-all": "^4.1.5", 66 | "optimize-css-assets-webpack-plugin": "^5.0.3", 67 | "postcss-loader": "^3.0.0", 68 | "postcss-px-to-viewport": "^1.1.1", 69 | "ramda": "^0.26.1", 70 | "react-app-polyfill": "^1.0.4", 71 | "uglifyjs-webpack-plugin": "^2.2.0", 72 | "webpack": "^4.41.2", 73 | "webpack-bundle-analyzer": "^3.6.0", 74 | "webpack-cli": "^3.3.10", 75 | "webpack-dev-middleware": "^3.7.2", 76 | "webpack-hot-middleware": "^2.25.0", 77 | "webpack-manifest-plugin": "^2.2.0", 78 | "webpackbar": "^4.0.0" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/container/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Carousel from "antd-mobile/lib/carousel"; 3 | import { ImgLazyLoad } from "@src/components"; 4 | 5 | const Wrapper = ({ title, children }) => { 6 | return ( 7 |
8 |
{title}
9 | {children} 10 |
11 | ); 12 | }; 13 | 14 | export const Banner = ({ data }) => { 15 | return ( 16 |
17 | 18 | {data.map(item => ( 19 |
20 | 24 |
25 | ))} 26 |
27 |
28 | ); 29 | }; 30 | 31 | export const Channel = ({ data }) => { 32 | return ( 33 |
    34 | {data.map(item => ( 35 |
  • 36 | 37 | {item.name} 38 |
  • 39 | ))} 40 |
41 | ); 42 | }; 43 | 44 | export const Brand = ({ data }) => { 45 | return ( 46 | 47 |
48 | {data.map(item => ( 49 |
50 | {item.name} 51 | {item.floor_price}元起 52 | 56 |
57 | ))} 58 |
59 |
60 | ); 61 | }; 62 | 63 | export const News = ({ data }) => { 64 | return ( 65 | 66 |
67 | {data.map(item => ( 68 |
69 | 73 |
{item.name}
74 |
¥{item.retail_price}
75 |
76 | ))} 77 |
78 |
79 |
80 | ); 81 | }; 82 | 83 | export const Hots = ({ data }) => { 84 | return ( 85 | 86 |
    87 | {data.map(item => ( 88 |
  • 89 |
    90 | 91 |
    92 |
    93 |
    {item.name}
    94 |
    {item.goods_brief}
    95 |
    ¥{item.retail_price}
    96 |
    97 |
  • 98 | ))} 99 |
100 |
101 | ); 102 | }; 103 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # REACT-MPA-SSR 2 | 3 | > REACT-MPA-SSR 以 React、Koa、React-loadable 搭建的 MPA 模式的 React 同构方案。 4 | > 5 | > demo 预览地址:http://39.108.84.221:8082 6 | 7 | ## Development 8 | 9 | ```powershell 10 | npm run dev 11 | ``` 12 | 13 | ## Production 14 | 15 | ```powershell 16 | npm run build 17 | ``` 18 | 19 | ## Analyze 20 | 21 | ```powershell 22 | npm run analyze 23 | ``` 24 | 25 | ## Directory 26 | 27 | ```npm 28 | nsp 29 | ├─ bin 服务端启动文件 30 | ├─ build webpack打包文件 31 | ├─ config 服务端配置文件 32 | ├─ lib 封装的api 33 | ├─ logs 服务日志文件 34 | ├─ service 服务端代码 35 | ├─ controller 控制器(页面渲染或者接口输出) 36 | ├─ decorator 控制器装饰器(路由中间装饰器) 37 | ├─ middleware 中间件 38 | ├─ middleware_app app使用的中间件 39 | ├─ utils 工具方法 40 | ├─ views koa-views渲染模板 41 | ├─ src 页面相关目录 42 | ├─ .nsp 自动生成的路由文件 43 | ├─ page 页面路由文件 44 | ├─ static 静态文件 45 | ├─ webpack webpack相关配置 46 | ├─ .nsp.js nsp配置文件 47 | ├─ postcss.config.js postcss配置文件 48 | ├─ jsconfig.json vscode配置文件 49 | ├─ nodemon.json nodemon配置文件 50 | └─ pm2.json pm2启动文件 51 | ``` 52 | 53 | ## Helloword 54 | 55 | #### 1.src/page/index.js 56 | 57 | ```js 58 | import React from 'react'; 59 | import { setInitModel } from '@lib/inject'; 60 | @setInitModel 61 | export default class Helloword extends React.Component { 62 | render() { 63 | return
{JSON.stringify(this.props)}
; 64 | } 65 | } 66 | ``` 67 | 68 | #### 2. service/controller/home.js 69 | 70 | ```js 71 | import { Controller, RequestMapping, NspRender } from '../decorator'; 72 | import AxiosHttp from '../utils/Http'; 73 | const Axios = new AxiosHttp({ 74 | timeout: 10000, 75 | baseURL: 'http://202.96.155.121:8888/api', 76 | }); 77 | @Controller 78 | class Home { 79 | @RequestMapping({ method: 'get', url: '/other' }) 80 | @NspRender({ title: 'helloword' }) 81 | async other(ctx) { 82 | const data = await Axios.httpRequest('/topic/list', { 83 | page: 1, 84 | size: 6, 85 | }); 86 | ctx.initModel = { ...data }; 87 | } 88 | } 89 | export default Home; 90 | ``` 91 | 92 | ## Api 93 | 94 | #### 1.@setInitModel 95 | 96 | > 服务端预请求数据与组件建立联系。(类似 redeux 的 connect) 97 | 98 | #### 2.@Controller 99 | 100 | > 1. 声明当前类是个控制器。 101 | > 102 | > 2. 在/service/controller 文件建立控制器,并在改使用@Controller。 103 | 104 | #### 3. @RequestMapping({ method = String, url = String }) 105 | 106 | > 1. 声明当前类的方法的请求方式和请求路由。 107 | > 2. method: Get、Post;url:请求路径。 108 | > 3. 注意:在当前类,方法名不要重复。 109 | 110 | #### 4.@NspRender({ title = String }) 111 | 112 | > 1. 声明当前方法是用于服务端渲染。 113 | > 2. title: 当前渲染的 html 的 title 名称。 114 | > 3. 使用 ctx.initModel = 'youData'注入数据。 115 | > 4. 使用 ctx.title = 'xxtitle'覆盖装饰器的 title。 116 | 117 | ## Complete 118 | 119 | > 1. 自动编译`src/page`目录下文件,生成路由文件 120 | > 2. 服务端日志 121 | > 3. 放弃 react-redux,简单使用 context 注入数据 122 | > 4. 热更新 123 | > 5. 按需加载 124 | -------------------------------------------------------------------------------- /webpack/base.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 4 | 5 | const HappyPack = require("happypack"); 6 | 7 | const ProgressBarPlugin = require("webpackbar"); 8 | 9 | const ManifestPlugin = require("webpack-manifest-plugin"); 10 | 11 | const FriendlyErrorsWebpackPlugin = require("friendly-errors-webpack-plugin"); 12 | 13 | const CopyWebpackPlugin = require("copy-webpack-plugin"); 14 | 15 | const { ReactLoadablePlugin } = require("nsploadable/webpack"); 16 | 17 | const rootPath = path.resolve(__dirname, "../"); 18 | 19 | const { cdnPrefix } = require("../.nsp"); 20 | 21 | const loadable = path.join(rootPath, "/build/loadable.json"); 22 | 23 | const manifest = path.join(rootPath, "/build/manifest.json"); 24 | 25 | const prefixName = 26 | process.env.NODE_ENV == "production" ? "[name].[contenthash:8]" : "[name]"; 27 | 28 | const publicPath = process.env.NODE_ENV == "production" ? cdnPrefix : "/"; 29 | module.exports = { 30 | entry: { 31 | assets: ["./src/index.js"] 32 | }, 33 | output: { 34 | filename: `Nsp_${prefixName}.js`, 35 | path: path.resolve(rootPath, "./build"), 36 | publicPath, 37 | chunkFilename: `${prefixName}.js` 38 | }, 39 | performance: { 40 | hints: false 41 | }, 42 | stats: "errors-only", 43 | module: { 44 | rules: [ 45 | { 46 | test: /\.js[x]?$/, 47 | exclude: /node_modules/, 48 | include: /src|lib/, 49 | use: "happypack/loader?id=js" 50 | }, 51 | { 52 | test: /\.(css|less)$/, 53 | include: /src/, 54 | use: [ 55 | MiniCssExtractPlugin.loader, 56 | "css-loader", 57 | "less-loader", 58 | "postcss-loader" 59 | ] 60 | }, 61 | { 62 | test: /\.(svg|woff2?|ttf|eot|jpe?g|png|gif)(\?.*)?$/i, 63 | exclude: /node_modules/, 64 | use: { 65 | loader: "url-loader", 66 | options: { 67 | limit: 1024, 68 | name: "img/[sha512:hash:base64:7].[ext]" 69 | } 70 | } 71 | } 72 | ] 73 | }, 74 | plugins: [ 75 | new ProgressBarPlugin({ summary: false }), 76 | 77 | new HappyPack({ 78 | id: "js", 79 | threads: 5, 80 | loaders: ["babel-loader?cacheDirectory=true"] 81 | }), 82 | 83 | new MiniCssExtractPlugin({ 84 | filename: `${prefixName}.css`, 85 | chunkFilename: `${prefixName}.css`, 86 | ignoreOrder: false 87 | }), 88 | 89 | new ManifestPlugin({ 90 | // 在热更新也需要打包到build 91 | writeToFileEmit: true, 92 | fileName: manifest 93 | }), 94 | 95 | // 需要Webpack告诉我们每个模块位于哪个捆绑包 96 | new ReactLoadablePlugin({ 97 | filename: loadable 98 | }), 99 | 100 | new FriendlyErrorsWebpackPlugin({ 101 | compilationSuccessInfo: { 102 | messages: ["You application is build successful~"], 103 | notes: [ 104 | "Some additionnal notes to be displayed unpon successful compilation" 105 | ] 106 | } 107 | }), 108 | 109 | new CopyWebpackPlugin([ 110 | { 111 | from: path.resolve(__dirname, "../src/static"), 112 | to: path.resolve(__dirname, "../build/static") 113 | } 114 | ]) 115 | ], 116 | resolve: { 117 | extensions: [".js", ".jsx", "css", "less", "png", "jpg"] 118 | }, 119 | optimization: { 120 | minimizer: [] 121 | } 122 | }; 123 | -------------------------------------------------------------------------------- /src/page/index.less: -------------------------------------------------------------------------------- 1 | #homePage { 2 | .bannerWrap { 3 | height: 400px; 4 | background: url(https://s1.hdslb.com/bfs/static/jinkela/international-home/asserts/bgm-nodata.png); 5 | background-size: 100% 100%; 6 | .banner { 7 | height: 400px; 8 | overflow: hidden; 9 | position: relative; 10 | img { 11 | width: 100%; 12 | height: 100%; 13 | } 14 | } 15 | } 16 | .channel { 17 | height: 90px; 18 | background: white; 19 | padding-top: 20px; 20 | display: flex; 21 | li { 22 | flex: 1; 23 | height: 100%; 24 | img { 25 | width: 50px; 26 | height: 50px; 27 | margin: 0 auto; 28 | display: block; 29 | } 30 | span { 31 | height: 40px; 32 | display: block; 33 | text-align: center; 34 | line-height: 40px; 35 | font-size: 24px; 36 | } 37 | } 38 | } 39 | .wrapper { 40 | margin-top: 20px; 41 | .title { 42 | height: 100px; 43 | line-height: 100px; 44 | text-align: center; 45 | background: white; 46 | letter-spacing: 2px; 47 | font-weight: 700; 48 | font-size: 30px; 49 | } 50 | .brandContent { 51 | display: flex; 52 | flex-wrap: wrap; 53 | background: white; 54 | div { 55 | width: 50%; 56 | height: 200px; 57 | position: relative; 58 | span.brandName { 59 | position: absolute; 60 | top: 20px; 61 | left: 20px; 62 | font-size: 20px; 63 | z-index: 2; 64 | } 65 | span.brandMoney { 66 | position: absolute; 67 | top: 50px; 68 | left: 20px; 69 | font-size: 20px; 70 | color: gray; 71 | z-index: 2; 72 | } 73 | img { 74 | position: relative; 75 | width: 100%; 76 | height: 200px; 77 | } 78 | } 79 | } 80 | .newsContent { 81 | display: flex; 82 | flex-wrap: wrap; 83 | background: white; 84 | padding: 0 20px; 85 | & > div { 86 | width: 50%; 87 | position: relative; 88 | padding: 0 20px; 89 | box-sizing: border-box; 90 | img { 91 | width: 315px; 92 | height: 315px; 93 | } 94 | div { 95 | text-align: center; 96 | font-size: 26px; 97 | } 98 | .newsName { 99 | color: #333; 100 | } 101 | .newsPrice { 102 | height: 60px; 103 | line-height: 60px; 104 | color: red; 105 | font-size: 30px; 106 | } 107 | } 108 | } 109 | .hotsContent { 110 | padding: 0 20px; 111 | background: white; 112 | li { 113 | height: 240px; 114 | display: flex; 115 | .left { 116 | width: 200px; 117 | height: 200px; 118 | padding: 20px 20px 20px 0; 119 | img { 120 | width: 100%; 121 | height: 100%; 122 | } 123 | } 124 | .right { 125 | flex: 1; 126 | height: 200px; 127 | padding: 20px 0; 128 | div { 129 | height: 33.333%; 130 | display: flex; 131 | align-items: center; 132 | } 133 | div:nth-child(1) { 134 | font-size: 30px; 135 | } 136 | div:nth-child(2) { 137 | color: darkgray; 138 | font-size: 28px; 139 | } 140 | div:nth-child(3) { 141 | color: red; 142 | font-size: 30px; 143 | } 144 | } 145 | } 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /lib/analyze.js: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import chalk from "chalk"; 3 | import path from "path"; 4 | import { pageIgnore } from "../.nsp"; 5 | class RouterAnalyze { 6 | constructor(entry, output, callback) { 7 | this.entry = entry; 8 | this.output = output; 9 | this.extensions = pageIgnore; 10 | this.callback = callback || null; 11 | this.jsRouterLinks = []; 12 | this.isRight = true; 13 | this.init(); 14 | } 15 | init() { 16 | this.compileDir(this.entry, ""); 17 | if (this.isRight) { 18 | const strings = this.writeTemplate(this.jsRouterLinks); 19 | this.writeFile(this.output, strings, this.callback); 20 | } else { 21 | console.log("文件夹编译错误!"); 22 | } 23 | } 24 | 25 | // 生成需要写的内容 26 | writeTemplate(jsRouterLinks) { 27 | let str = 28 | 'import React, { Fragment } from "react";\n' + 29 | 'import { Route } from "react-router-dom";\n' + 30 | 'import Loadable from "nsploadable";\n\n'; 31 | let routerStr = "const routes = [\n"; 32 | jsRouterLinks.forEach(item => { 33 | const componentName = `Nsp${item.replace(/\//g, "_")}`; 34 | const pathName = `../page${item}`; 35 | str += this.templateLoadable(componentName, pathName) + "\n\n"; 36 | routerStr += this.templateRoutes(componentName, item); 37 | }); 38 | 39 | return str + routerStr + "]\n\n" + this.templateApp(); 40 | } 41 | 42 | templateLoadable(componentName, path) { 43 | return ( 44 | `const ${componentName} = Loadable({\n` + 45 | ` loader: () => import(/* webpackChunkName: '${componentName}' */ '${path}')\n` + 46 | `});` 47 | ); 48 | } 49 | 50 | templateRoutes(componentName, path) { 51 | return ( 52 | " {\n" + 53 | ` path: "${path}",\n` + 54 | ` component: <${componentName} />\n` + 55 | ` },\n` 56 | ); 57 | } 58 | 59 | templateApp() { 60 | return ( 61 | "class App extends React.Component {\n" + 62 | " render () {\n" + 63 | " return (\n" + 64 | " \n" + 65 | " {routes.map(item => (\n" + 66 | " \n" + 67 | " {item.component}\n" + 68 | " \n" + 69 | " ))}\n" + 70 | " \n" + 71 | " );\n" + 72 | " }\n" + 73 | "}\n\n" + 74 | "export default App;" 75 | ); 76 | } 77 | 78 | // compileDir 79 | compileDir(entry, prefix) { 80 | let { code, files } = this.isHasDir(entry); 81 | files = files.filter(item => item.endsWith(".js") || !item.includes(".")); 82 | if (this.extensions.length > 0) { 83 | for (let i = 0; i < files.length; i++) { 84 | let item = files[i]; 85 | for (let exItem of this.extensions) { 86 | if (item === exItem) { 87 | files.splice(i, 1); 88 | i--; 89 | } 90 | } 91 | } 92 | } 93 | if (code) { 94 | // 有文件 95 | if (files.length > 0) { 96 | for (let item of files) { 97 | // 判断是否有重复的 98 | for (let _item of files) { 99 | if (item !== _item && item.includes(_item)) { 100 | console.log( 101 | chalk.red( 102 | `× ${entry}文件夹的${item}和${_item}重复命名!请删除其一!` 103 | ) 104 | ); 105 | this.isRight = false; 106 | } 107 | } 108 | // 没有重复 109 | // 以index.js结尾的文件 110 | if (item.endsWith("index.js")) { 111 | this.jsRouterLinks.push(prefix == "" ? "/" : prefix); 112 | } else if (item.endsWith(".js")) { 113 | this.jsRouterLinks.push(prefix + `/${item.replace(".js", "")}`); 114 | } else if (item) { 115 | const nextPath = path.join(entry, `/${item}`); 116 | this.compileDir(nextPath, prefix + `/${item}`); 117 | } 118 | } 119 | } else { 120 | // 没有文件 121 | console.log(chalk.yellow(`- ${entry}文件夹没有文件~`)); 122 | } 123 | } else { 124 | console.log(chalk.red(`× ${entry}文件夹解析错误!`)); 125 | this.isRight = false; 126 | } 127 | } 128 | 129 | // 写文件内容 130 | writeFile(outputFile, content, callback) { 131 | let result = ""; 132 | try { 133 | result = fs.readFileSync(outputFile, "utf8"); 134 | } catch (e) { 135 | fs.mkdirSync("src/.nsp"); 136 | } 137 | if (result === content) { 138 | console.log(chalk.green("√ 文件不需要更新!")); 139 | callback && callback(); 140 | } else { 141 | fs.writeFile(outputFile, content, err => { 142 | console.log(chalk.green("√ 文件已生成!")); 143 | callback && callback(); 144 | if (err) { 145 | console.log(chalk.red("× 文件生成失败!")); 146 | throw err; 147 | } 148 | }); 149 | } 150 | } 151 | 152 | // 判断是否存在目录 153 | isHasDir(inPath) { 154 | try { 155 | const files = fs.readdirSync(inPath); 156 | return { code: true, files }; 157 | } catch (e) { 158 | return { code: false, files: [] }; 159 | } 160 | } 161 | } 162 | 163 | export default RouterAnalyze; 164 | --------------------------------------------------------------------------------