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-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官方案例提供了周边框架的搭建实例,方便开发人员配置相关框架和插件,如下所示:
首先我们先来安装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)@withReduxSagaclass 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 middlewareconst sagaMiddleware = createSagaMiddleware();// 组合middlewareconst 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。
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-s @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-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 livesconst 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!我们明明已经按照with-ant-design-less配置好了同时支持ant-design和hiynn-design为什么还报错?
接着我就找问题原因,在segmentfault和next-plugins 的issues 上面提问,最后发现next.js不支持.pcss,因此我就把hiynn-design的库从.pcss全部变成了.scss,这样重新编译并打包发布并安装,运行起来就没这个错了,但是又有个新的bug,真的是一波未平一波又起啊!
接着我就又找问题原因,同样我又在segmentfault和next-plugins的issues上面提问,最后发现是因为我两个库的.babelrc文件里面按需加载的libraryDirecory不一致导致的,hiynn-design库使用的libraryDirecory是'es',而next.js项目使用的libraryDirecory是'lib',然后我统一了一下两个libraryDirecory就没问题了,代码如下:
[ "import", { "libraryName": "antd", "style": true }, "antd"],
这个问题是因为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 livesconst 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 采坑录(服务端渲染)
联系客服