├── .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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------