使用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
运行起来试试
配置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官方案例提供了周边框架的搭建实例,方便开发人员配置相关框架和插件,如下所示:
首先我们先来安装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 (
);
}
}
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包含那些插件,如下所示:
这里就不一一详细说明了,我们只举几个重要的安装包来说明下。
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 '@'
然后我们运行起来试试,报了一个bug!我们明明已经按照with-ant-design-less配置好了同时支持ant-design和hiynn-design为什么还报错?
接着我就找问题原因,在segmentfault和next-plugins 的issues 上面提问,最后发现next.js不支持.pcss,因此我就把hiynn-design的库从.pcss全部变成了.scss,这样重新编译并打包发布并安装,运行起来就没这个错了,但是又有个新的bug,真的是一波未平一波又起啊!
Bug:SyntaxError:Unexpected string
接着我就又找问题原因,同样我又在segmentfault和next-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
这个问题是因为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 采坑录(服务端渲染)