4

使用create-next-app初始化项目

create-next-app和react官方提供的create-react-app(CRA)非常像,next.js官方也自带了create-next-app(CNA),所以直接安装就好。

sudo yarn global add create-next-app

安装好之后就可以创建项目了

create-next-app project-name

生成之后的目录结构如下

    |-- .gitignore
    |-- package.json
    |-- yarn.lock
    |-- components
    |   |-- nav.js
    |-- pages
    |   |-- index.js
    |-- public
        |-- favicon.ico

我们可以查看下package.json

package.json

{
  "name": "next-demo4",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start"
  },
  "dependencies": {
    "next": "9.1.4",
    "react": "16.12.0",
    "react-dom": "16.12.0"
  }
}

然后执行yarn dev运行起来试试

image.png

配置koa路由

安装好了之后,我们可以设置成koa路由,首先添加koa 和 koa-router依赖包,然后在根目录下添加server.js文件,如下:

yarn add koa koa-router

server.js

const Koa = require("koa");
const next = require("next");
const Router = require("koa-router");

const port = parseInt(process.env.PORT, 10) || 3003;
const dev = process.env.NODE_ENV !== "production";
const app = next({ dev });
const handle = app.getRequestHandler();

app.prepare().then(() => {
  const server = new Koa();
  const router = new Router();

  router.get("/", async ctx => {
    await app.render(ctx.req, ctx.res, "/", ctx.query);
    ctx.respond = false;
  });

  router.all("*", async ctx => {
    await handle(ctx.req, ctx.res);
    ctx.respond = false;
  });

  server.use(async (ctx, next) => {
    ctx.res.statusCode = 200;
    await next();
  });

  server.use(router.routes());
  server.listen(port, () => {
    console.log(`> Ready on http://localhost:${port}`);
  });
});

修改package.json文件,添加如下代码:

"scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
+   "koa": "node server.js -p 3003"
  },

我们现在执行yarn koa试试,运行成功!

通过next.js官方提供的example案例来配置redux和immutable.js

next.js官方案例提供了周边框架的搭建实例,方便开发人员配置相关框架和插件,如下所示:
image.png

首先我们先来安装redux相关依赖包,如下

yarn add redux redux-actions redux-saga redux-logger

接着我们找一下我们要搭建的redux和immutable.js相关实例:with-redux-saga和with-immutable-redux-wrapper,然后自己一点点的去配置,当然中间肯定遇到了各种各样的问题,这里就不详细说明了,只展示配置好运行起来没问题的代码分享给大家,如下所示:

_app.js

import React from "react";
import { Provider } from "react-redux";
import App, { Container } from "next/app";
import withRedux from "next-redux-wrapper";
import withReduxSaga from "next-redux-saga";
import { fromJS } from "immutable";
import configureStore from "../service/redux/store";
import "../assets/styles/index.css";
import "antd/dist/antd.css";

@withRedux(configureStore)
@withReduxSaga
class NextApp extends App {
  static async getInitialProps({ Component, ctx }) {
    let pageProps = {};
    if (Component.getInitialProps) {
      pageProps = await Component.getInitialProps({ ctx });
    }
    return { pageProps };
  }

  render() {
    const { Component, pageProps, store } = this.props;
    return (
      <Container>
        <Provider store={store}>
          <Component {...pageProps} />
        </Provider>
      </Container>
    );
  }
}

export default NextApp;

store.js

import { createStore, compose, applyMiddleware } from "redux";
import createSagaMiddleware from "redux-saga";
import logger from "redux-logger";
import rootReducer from "./reducers";
import rootSaga from "./sagas";
import { fromJS } from "immutable";

// create the saga middleware
const sagaMiddleware = createSagaMiddleware();

// 组合middleware
const middleWares = [sagaMiddleware, logger];

export default function configureStore(preloadedState = fromJS({})) {
  const store = createStore(rootReducer, applyMiddleware(...middleWares));

  store.sagaTask = sagaMiddleware.run(rootSaga);
  return store;
}

reducers.js

import { combineReducers } from "redux";
import { autoCombineReducer } from "../../utils/autoCombineRedux";

export default combineReducers(autoCombineReducer());

sagas.js

import { all, fork } from "redux-saga/effects";
import { autoCombineSaga } from "../../utils/autoCombineRedux";

/*添加对action的监听 */
export default function* rootSaga() {
  yield all(autoCombineSaga());
}

这里我说明下reducers.js和sagas.js用到的方法:autoCombineReducer和autoCombineSaga,以前我们写reducer.js或者saga.js是把相关reducer或者saga文件import进来,然后注入到combineReducers()方法或者yield all()里面,好处是直观,坏处就是每次写一个reducer或者saga文件就要在这里加一下,是不是觉得非常麻烦,索性自己写一个自动注册进combineReducers()或者yield all()的方法,这个方法利用webpack的CONTEXT(require.context)把文件夹下名称为reducer.js或者saga.js文件自动加载进来,详细请看下面代码:

autoCombineRedux.js

import { all, fork } from "redux-saga/effects";

// 查询所有文件夹下的所有文件名,以文件名数组形式返回
const getContext = (path, type) => {
  let CONTEXT = type == "reducer" ? require.context(".", true, /reducer\.js$/) : require.context(".", true, /saga\.js$/);
  if (path == "../") {
    CONTEXT = type == "reducer" ? require.context("../", true, /reducer\.js$/) : require.context("../", true, /saga\.js$/);
  }
  if (path == "../../") {
    CONTEXT = type == "reducer" ? require.context("../../", true, /reducer\.js$/) : require.context("../../", true, /saga\.js$/);
  }
  return CONTEXT;
};

export const autoCombineReducer = (path = "../") => {
  let CONTEXT = getContext(path, "reducer");

  // 获取完组合成reducer对象
  const importReducer = req => (obj, path) => {
    //从路径中获取reducer name
    // output:[0: "demo",groups: undefined,index: 7,input: "./demo/reducer.js"]
    const [componentName] = path.match(/\w+(?=\/reducer\.js$)/);

    // 组合reducer对象
    const reducer = {
      [`${componentName}Reducer`]: req(path).default
    };

    return Object.assign({}, obj, reducer);
  };

  // 从CONTEXT路径数组中组合成reducer对象返回
  const getReducers = ctx => ctx.keys().reduce(importReducer(ctx), {});
  return getReducers(CONTEXT);
};

export const autoCombineSaga = (path = "../") => {
  let CONTEXT = getContext(path, "saga");
  // 获取完组合成reducer对象
  const forkSaga = req => path => {
    //从路径中获取reducer name
    // output:[0: "demo",groups: undefined,index: 7,input: "./demo/demo.reducer.js"]
    const [componentName] = path.match(/\w+(?=\/saga\.js$)/);
    return fork(req(path).default);
  };

  // 从CONTEXT路径数组中组合成reducer对象返回
  const getSagas = ctx => ctx.keys().map(forkSaga(ctx));
  return getSagas(CONTEXT);
};

大家有没有发现其实上面已经配置好了immutable.js,所以我们就可以直接在reduer文件里面使用immutable对象了,如下所示:

reducer.js

import { handleActions } from "redux-actions";
import { authTypes } from "./action";
import moment from "moment";
import { Map, fromJS, merge } from "immutable";

const initState = fromJS({
  user: null,
  token: ""
});

const authReducer = handleActions(
  {
    [authTypes.AUTH_SUCCESS]: (state, action) => {
      return state.merge({
        user: action.data.user,
        token: action.data.token
      });
    },
    [authTypes.SIGN_OUT]: (state, action) => {
      return state.merge({
        user: null,
        token: ""
      });
    }
  },
  initState
);

export default authReducer;

这样就完成了redux和immutable.js的配置了,下面我们来配置antd。

使用next-plugins来配置antd和hiynn-design

ant-design
antd 就不需要我多说明了吧,国内顶级大厂的杰出之作。

hiynn-design
hiynn-design是我公司(海云)前(我)端(创)团(建)队(的)创建的UI库,分为标准化组件和可视化组件。

同样的我们在next.js的example里面找到:with-ant-design-less 我们就100%参考这个实例来配置antd,首先我们来添加下相关依赖包:

yarn add @zeit/next-css @zeit/next-sass @zeit/next-less next-compose-plugins less-vars-to-js next-images 

yarn add babel-plugin-import @babel/plugin-proposal-decorators @babel/plugin-proposal-do-expressions @babel/plugin-proposal-class-properties

回过头来我们再来看看next-plugins包含那些插件,如下所示:

image.png

这里就不一一详细说明了,我们只举几个重要的安装包来说明下。

next-images
在SPA项目里面我们使用import或者require来加载图片,但是next.js不是那么简单的,它会提供你一个public文件夹来放静态图片,然后写上绝对地址,这样一来动态添加图片就变得麻烦,因此就有next-images这个依赖包的出现了,这个包的作用是让你像import require那样引入你的图片。

next-compose-plugins
当我们的next.config.js文件需要同时配置多个包的时候就会让每个单独配置的包分散开了,这样不利于维护,因此就出现了next-compose-plugins把各个分散的包集合在一起,具体的请查看next-compose-plugins

接着我们就按照with-ant-design-less来配置.babelrc和next.config.js文件,这里我们同时支持了ant-design和hiynn-design,详细代码如下:

.babelrc

{
  "presets": [
    "@babel/preset-react", "next/babel"
  ],
  "plugins": [
    "@babel/plugin-proposal-do-expressions",
    [
      "@babel/plugin-proposal-decorators",
      {
        "legacy": true
      }
    ],
    ["@babel/plugin-proposal-class-properties", {
      "loose": true
    }],
    //按需加载antd样式
    [
      "import", {
        "libraryName": "antd",
        "style": true
      },
      "antd"
    ],
    //按需加载hiynnd样式
    [
      "import",
      {
        "libraryName": "hiynn-design",
        "style": true
      },
      "hiynn-design"
    ]
  ]
}

next.config.js

const withCss = require("@zeit/next-css");
const withSass = require("@zeit/next-sass");
const withPlugins = require("next-compose-plugins");
const withLess = require("@zeit/next-less");
const lessToJS = require("less-vars-to-js");
const fs = require("fs");
const path = require("path");
const withImages = require("next-images");

// Where your antd-custom.less file lives
const themeVariables = lessToJS(fs.readFileSync(path.resolve(__dirname, "./src/assets/styles/antd-custom.less"), "utf8"));

module.exports = withPlugins([withCss, withSass, withLess, withImages], {
  cssLoaderOptions: {
    importLoaders: 1
  },
  lessLoaderOptions: {
    javascriptEnabled: true,
    modifyVars: themeVariables // make your antd custom effective
  },
  webpack: (config, { isServer }) => {
    if (isServer) {
      const antStyles = /antd\/.*?\/style.*?/;
      const hiynnStyles = /hiynn-design\/.*?\/style.*?/;
      const origExternals = [...config.externals];
      config.externals = [
        (context, request, callback) => {
          if (request.match(antStyles) || request.match(hiynnStyles)) {
            return callback();
          }
          if (typeof origExternals[0] === "function") {
            origExternals[0](context, request, callback);
          } else {
            callback();
          }
        },
        ...(typeof origExternals[0] === "function" ? [] : origExternals)
      ];
      config.module.rules.push(
        {
          test: antStyles,
          use: "null-loader"
        },
        {
          test: hiynnStyles,
          use: "null-loader"
        }
      );
    }
    return config;
  }
});

请注意,Next.js无法使用css-loader。 请参阅官方页面上的警告
警告:不建议添加加载程序以支持新的文件类型(css,less,svg等),因为只有客户端代码通过webpack捆绑在一起,因此在初始服务器渲染中不起作用。 Babel插件是一个不错的选择,因为它们在服务器/客户端渲染之间一致地应用

这也算是配置next.js中的一个小插曲吧,大家知道就好,因为我们上面已经解决了这个问题。

Bug:Module parse failed: Unexpected character '@'

image

然后我们运行起来试试,报了一个bug!我们明明已经按照with-ant-design-less配置好了同时支持ant-design和hiynn-design为什么还报错?

接着我就找问题原因,在segmentfaultnext-plugins 的issues 上面提问,最后发现next.js不支持.pcss,因此我就把hiynn-design的库从.pcss全部变成了.scss,这样重新编译并打包发布并安装,运行起来就没这个错了,但是又有个新的bug,真的是一波未平一波又起啊!

Bug:SyntaxError:Unexpected string

image

接着我就又找问题原因,同样我又在segmentfaultnext-plugins的issues上面提问,最后发现是因为我两个库的.babelrc文件里面按需加载的libraryDirecory不一致导致的,hiynn-design库使用的libraryDirecory是'es',而next.js项目使用的libraryDirecory是'lib',然后我统一了一下两个libraryDirecory就没问题了,代码如下:

[
  "import", {
    "libraryName": "antd",
    "style": true
  },
  "antd"
],

Bug:SyntaxError:Invalid or unexpected token

image.png
这个问题是因为antd .less文件后缀引起的,在next.config.js加上下面这段代码就OK了

next.config.js

const withCss = require("@zeit/next-css");
const withSass = require("@zeit/next-sass");
const withPlugins = require("next-compose-plugins");
const withLess = require("@zeit/next-less");
const lessToJS = require("less-vars-to-js");
const fs = require("fs");
const path = require("path");
const withImages = require("next-images");

// Where your antd-custom.less file lives
const themeVariables = lessToJS(fs.readFileSync(path.resolve(__dirname, "./src/assets/styles/antd-custom.less"), "utf8"));
+ require.extensions[".less"] = () => {};
module.exports = withPlugins([withCss, withSass, withLess, withImages], {
  cssLoaderOptions: {
    importLoaders: 1
  },
  lessLoaderOptions: {
    javascriptEnabled: true,
    modifyVars: themeVariables // make your antd custom effective
  },
  webpack: (config, { isServer }) => {
    if (isServer) {
      const antStyles = /antd\/.*?\/style.*?/;
      const hiynnStyles = /hiynn-design\/.*?\/style.*?/;
      const origExternals = [...config.externals];
      config.externals = [
        (context, request, callback) => {
          if (request.match(antStyles) || request.match(hiynnStyles)) {
            return callback();
          }
          if (typeof origExternals[0] === "function") {
            origExternals[0](context, request, callback);
          } else {
            callback();
          }
        },
        ...(typeof origExternals[0] === "function" ? [] : origExternals)
      ];
      config.module.rules.push(
        {
          test: antStyles,
          use: "null-loader"
        },
        {
          test: hiynnStyles,
          use: "null-loader"
        }
      );
    }
    return config;
  }
});

最后这些问题解决后我们再运行就不再报错了,运行正常!虽然我写这篇文章只用了2个小时,但是解决这些问题我足足花了一个多月。

总结

1、next.js对css-loader支持不友好,应该是所有node.js基于后台的通病了
2、配置redux和immutable网上有现成的案例可以根据这些案例进行配置
3、配置antd和hiynnd是最辛苦的,虽然最后成功了
4、postcss一般做为辅助sass或者less库使用最好,而不要单独使用,比如我的hiynn-design库一开始使用的全是.pcss结尾,然后就变成.scss。

参考

less、sass和postcss总结
next-plugins
ModuleParseError: Module parse failed: Unexpected character '@'
SyntaxError: Unexpected string
nextjs-starter-kit
nextjs-starter
哔哩哔哩(B站)的前端之路
PostCSS深入学习: PostCSS和Sass、Stylus或LESS一起使用
Next.js 使用指南2-路由与加载
next-images
next.js 采坑录(服务端渲染)


Awbeci
3.1k 声望213 粉丝

Awbeci