使用create-next-app初始化項(xiàng)目
create-next-app和react官方提供的create-react-app(CRA)非常像,next.js官方也自帶了create-next-app(CNA),所以直接安裝就好。
sudo yarn global add create-next-app
安裝好之后就可以創(chuàng)建項(xiàng)目了
create-next-app project-name
生成之后的目錄結(jié)構(gòu)如下
|-- .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"
}
}
然后執(zhí)行yarn dev 運(yùn)行起來(lái)試試

配置koa路由
安裝好了之后,我們可以設(shè)置成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"
},
我們現(xiàn)在執(zhí)行yarn koa 試試,運(yùn)行成功!
通過(guò)next.js官方提供的example案例來(lái)配置redux和immutable.js
next.js官方案例提供了周邊框架的搭建實(shí)例,方便開(kāi)發(fā)人員配置相關(guān)框架和插件,如下所示:

首先我們先來(lái)安裝redux相關(guān)依賴包,如下
yarn add redux redux-actions redux-saga redux-logger
接著我們找一下我們要搭建的redux和immutable.js相關(guān)實(shí)例:with-redux-saga和with-immutable-redux-wrapper,然后自己一點(diǎn)點(diǎn)的去配置,當(dāng)然中間肯定遇到了各種各樣的問(wèn)題,這里就不詳細(xì)說(shuō)明了,只展示配置好運(yùn)行起來(lái)沒(méi)問(wèn)題的代碼分享給大家,如下所示:
_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";
/*添加對(duì)action的監(jiān)聽(tīng) */
export default function* rootSaga() {
yield all(autoCombineSaga());
}
這里我說(shuō)明下reducers.js和sagas.js用到的方法:autoCombineReducer和autoCombineSaga,以前我們寫reducer.js或者saga.js是把相關(guān)reducer或者saga文件import進(jìn)來(lái),然后注入到combineReducers()方法或者yield all()里面,好處是直觀,壞處就是每次寫一個(gè)reducer或者saga文件就要在這里加一下,是不是覺(jué)得非常麻煩,索性自己寫一個(gè)自動(dòng)注冊(cè)進(jìn)combineReducers()或者yield all()的方法,這個(gè)方法利用webpack的CONTEXT(require.context)把文件夾下名稱為reducer.js或者saga.js文件自動(dòng)加載進(jìn)來(lái),詳細(xì)請(qǐng)看下面代碼:
autoCombineRedux.js
import { all, fork } from "redux-saga/effects";
// 查詢所有文件夾下的所有文件名,以文件名數(shù)組形式返回
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對(duì)象
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對(duì)象
const reducer = {
[`${componentName}Reducer`]: req(path).default
};
return Object.assign({}, obj, reducer);
};
// 從CONTEXT路徑數(shù)組中組合成reducer對(duì)象返回
const getReducers = ctx => ctx.keys().reduce(importReducer(ctx), {});
return getReducers(CONTEXT);
};
export const autoCombineSaga = (path = "../") => {
let CONTEXT = getContext(path, "saga");
// 獲取完組合成reducer對(duì)象
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路徑數(shù)組中組合成reducer對(duì)象返回
const getSagas = ctx => ctx.keys().map(forkSaga(ctx));
return getSagas(CONTEXT);
};
大家有沒(méi)有發(fā)現(xiàn)其實(shí)上面已經(jīng)配置好了immutable.js,所以我們就可以直接在reduer文件里面使用immutable對(duì)象了,如下所示:
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的配置了,下面我們來(lái)配置antd。
使用next-plugins來(lái)配置antd和hiynn-design
ant-design antd 就不需要我多說(shuō)明了吧,國(guó)內(nèi)頂級(jí)大廠的杰出之作。
hiynn-design hiynn-design是我公司(海云)前(我)端(創(chuàng))團(tuán)(建)隊(duì)(的)創(chuàng)建的UI庫(kù),分為標(biāo)準(zhǔn)化組件和可視化組件。
同樣的我們?cè)趎ext.js的example里面找到:with-ant-design-less 我們就100%參考這個(gè)實(shí)例來(lái)配置antd,首先我們來(lái)添加下相關(guān)依賴包:
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-s @babel/plugin-proposal-class-properties
回過(guò)頭來(lái)我們?cè)賮?lái)看看next-plugins包含那些插件,如下所示:

這里就不一一詳細(xì)說(shuō)明了,我們只舉幾個(gè)重要的安裝包來(lái)說(shuō)明下。
next-images 在SPA項(xiàng)目里面我們使用import或者require來(lái)加載圖片,但是next.js不是那么簡(jiǎn)單的,它會(huì)提供你一個(gè)public文件夾來(lái)放靜態(tài)圖片,然后寫上絕對(duì)地址,這樣一來(lái)動(dòng)態(tài)添加圖片就變得麻煩,因此就有next-images這個(gè)依賴包的出現(xiàn)了,這個(gè)包的作用是讓你像import require那樣引入你的圖片。
next-compose-plugins 當(dāng)我們的next.config.js文件需要同時(shí)配置多個(gè)包的時(shí)候就會(huì)讓每個(gè)單獨(dú)配置的包分散開(kāi)了,這樣不利于維護(hù),因此就出現(xiàn)了next-compose-plugins把各個(gè)分散的包集合在一起,具體的請(qǐng)查看next-compose-plugins
接著我們就按照with-ant-design-less來(lái)配置.babelrc和next.config.js文件,這里我們同時(shí)支持了ant-design和hiynn-design,詳細(xì)代碼如下:
.babelrc
{
"presets": [
"@babel/preset-react", "next/babel"
],
"plugins": [
"@babel/plugin-proposal-do-s",
[
"@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;
}
});
請(qǐng)注意,Next.js無(wú)法使用css-loader。 請(qǐng)參閱官方頁(yè)面上的警告
警告:不建議添加加載程序以支持新的文件類型(css,less,svg等),因?yàn)橹挥锌蛻舳舜a通過(guò)webpack捆綁在一起,因此在初始服務(wù)器渲染中不起作用。 Babel插件是一個(gè)不錯(cuò)的選擇,因?yàn)樗鼈冊(cè)诜?wù)器/客戶端渲染之間一致地應(yīng)用
這也算是配置next.js中的一個(gè)小插曲吧,大家知道就好,因?yàn)槲覀兩厦嬉呀?jīng)解決了這個(gè)問(wèn)題。
Bug:Module parse failed: Unexpected character '@'

然后我們運(yùn)行起來(lái)試試,報(bào)了一個(gè)bug!我們明明已經(jīng)按照with-ant-design-less配置好了同時(shí)支持ant-design和hiynn-design為什么還報(bào)錯(cuò)?
接著我就找問(wèn)題原因,在segmentfault和next-plugins 的issues 上面提問(wèn),最后發(fā)現(xiàn)next.js不支持.pcss,因此我就把hiynn-design的庫(kù)從.pcss全部變成了.scss,這樣重新編譯并打包發(fā)布并安裝,運(yùn)行起來(lái)就沒(méi)這個(gè)錯(cuò)了,但是又有個(gè)新的bug,真的是一波未平一波又起??!
Bug:SyntaxError:Unexpected string

接著我就又找問(wèn)題原因,同樣我又在segmentfault和next-plugins的issues上面提問(wèn),最后發(fā)現(xiàn)是因?yàn)槲覂蓚€(gè)庫(kù)的.babelrc文件里面按需加載的libraryDirecory不一致導(dǎo)致的,hiynn-design庫(kù)使用的libraryDirecory是'es',而next.js項(xiàng)目使用的libraryDirecory是'lib',然后我統(tǒng)一了一下兩個(gè)libraryDirecory就沒(méi)問(wèn)題了,代碼如下:
[
"import", {
"libraryName": "antd",
"style": true
},
"antd"
],
Bug:SyntaxError:Invalid or unexpected token
 這個(gè)問(wèn)題是因?yàn)閍ntd .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;
}
});
最后這些問(wèn)題解決后我們?cè)龠\(yùn)行就不再報(bào)錯(cuò)了,運(yùn)行正常!雖然我寫這篇文章只用了2個(gè)小時(shí),但是解決這些問(wèn)題我足足花了一個(gè)多月。
總結(jié)
1、next.js對(duì)css-loader支持不友好,應(yīng)該是所有node.js基于后臺(tái)的通病了 2、配置redux和immutable網(wǎng)上有現(xiàn)成的案例可以根據(jù)這些案例進(jìn)行配置 3、配置antd和hiynnd是最辛苦的,雖然最后成功了 4、postcss一般做為輔助sass或者less庫(kù)使用最好,而不要單獨(dú)使用,比如我的hiynn-design庫(kù)一開(kāi)始使用的全是.pcss結(jié)尾,然后就變成.scss。
參考
less、sass和postcss總結(jié) next-plugins ModuleParseError: Module parse failed: Unexpected character '@' SyntaxError: Unexpected string nextjs-starter-kit nextjs-starter 嗶哩嗶哩(B站)的前端之路 PostCSS深入學(xué)習(xí): PostCSS和Sass、Stylus或LESS一起使用 Next.js 使用指南2-路由與加載 next-images next.js 采坑錄(服務(wù)端渲染)
|