Codeitup 码起来 - 前端学习随笔

Next.js项目搭建实录


Next.js项目搭建实录

本文档随手记录基于Next.js项目的搭建过程,融合各个功能的过程中遇到的问题,评估是否可以满足开发需要,同时考虑便利性和稳定性。记录的顺序部分前后。

使用styled-components

Next.js支持css-in-js,但是写法别扭所以暂不考虑,使用React比较流行的styled-components,把HTML标签装饰成组件的形式,复用度高,自带代码分割和前缀补全,对于“主题”支持更好,维护起来也很方便。编程式的样式书写,也有一定的SCSS或less的优势。webstorm安装一下styled-components支持,就可以方便的书写样式表。

npm install –save styled-components

为了支持Next.js的SSR,还要手动创建(如果没有的话).babelrc文件,开启styled-components的ssr支持:

{"presets": ["next/babel"],"plugins": [["styled-components",{"ssr": true}]]}

扩展Next.js的App植入<ThemeProvider>,在这里使用createGlobalStyle创建全局样式:

import App from 'next/app'
import React from 'react'
import {ThemeProvider, createGlobalStyle} from 'styled-components'

const theme = {
  colors: {
    primary: '#0070f3'
  }
};

const GlobalStyle = createGlobalStyle`
  body {
    padding: 0;
    margin: 0;
  }
`;

export default class MyApp extends App {
  render() {
    const {Component, pageProps} = this.props;
    return (
      <ThemeProvider theme={theme}>
        <React.Fragment>
          <GlobalStyle/>
          <Component {...pageProps} />
        </React.Fragment>
      </ThemeProvider>
    )
  }
}

扩展Next.js的Document注入用于加载样式表的head(讲道理这个并没有看懂,但是styled-components的官方文档让我们直接copy这些逻辑,那我就不管了):

import Document from 'next/document'
import {ServerStyleSheet} from 'styled-components'

export default class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const sheet = new ServerStyleSheet();
    const originalRenderPage = ctx.renderPage;

    try {
      ctx.renderPage = () =>
        originalRenderPage({
          enhanceApp: App => props => sheet.collectStyles(<App {...props} />)
        });

      const initialProps = await Document.getInitialProps(ctx);
      return {
        ...initialProps,
        styles: (
          <>
            {initialProps.styles}
            {sheet.getStyleElement()}
          </>
        )
      }
    } finally {
      sheet.seal()
    }
  }
}

使用iconfont

由于iconfont用到了svg和ttf等特殊文件,所以需要一些特殊的帮助。通过查询,我们需要next-font来扩展webpack配置。

const withSass = require('@zeit/next-sass');
const withCSS = require('@zeit/next-css');
const withFonts = require('next-fonts');
module.exports = withFonts(withCSS(withSass({
    enableSvg: true,
    webpack(config, options) {
        return config;
    }
})));

注意处理的顺序,现将sass处理成css,在处理css的时候需要引用字体等,所以顺序是withFonts>withCSS>withSass。并且配置要注意webpack属性,将config返回出来。处理好loader,就可以在_app.tsx中进行全局引用了。

使用路由

Next.js的路由系统基于/pages目录下的文件组织,和请求进行对应,参数路由以[params].js对应。

测试发现:在/pages目录下所有的子目录及组件文件,都会被映射成路由。比如/pages/post/components/Button.js会响应/post/components/Button请求。

/pages目录下所有的非“_”开头的文件,都将按其相对路径被映射成路由,所以页面中包含的组件,需要单独出去进行维护。

使用react-intl国际化

官方范例

基本用法

按照react-intl的要求准备语言文件,要注意的是react-intl使用的messages是一个普通的平铺的对象,但是键可以使用路径形式,即‘common.confirm.ok’。

module.exports = {title: 'Next-Demo 标题',greeting: '欢迎!','common.confirm.ok': '确认'};

因为下面的例子多语言文件是从服务端加载的,所以使用CommonJS形式导出。为了更好的维护多语言文件,可以引用flat库将其平铺。

next-with-react-intl这个例子,主要是在服务端结合了国际化的部分。先来看server.js的部分代码:

server.get('*', (req, res) => {
  const accept = accepts(req);
  const locale = (accept.language(accept.languages(supportedLanguages)) || 'zh-CN').split("-")[0];
  req.locale = locale;
  req.localeDataScript = getLocaleDataScript(locale);
  req.messages = flat(getMessages(locale));
  return handle(req, res);
});
const localeDataCache = new Map();
const getLocaleDataScript = locale => {
  const lang = locale.split('-')[0];
  if (!localeDataCache.has(lang)) {
    const localeDataFile = require.resolve(`@formatjs/intl-relativetimeformat/dist/locale-data/${lang}`);
    const localeDataScript = readFileSync(localeDataFile, 'utf8');
    localeDataCache.set(lang, localeDataScript)
  }
  return localeDataCache.get(lang)
};

理论上服务端跟react-intl没关系,而是跟intl国际化有关系。这里负责解析所有的请求,并通过accepts库,根据请求信息来决定应当使用的语种,然后通过getLocaleDataScript方法,将formatjs中对应语种的通用多语言内容提取出来,再将对应语言的多语言文件内容读取出来,统统塞到req中,这些信息就会被带到浏览器端。下面的工作交给_app.tsx和_document.tsx。

// _app.tsx
export default class MyApp extends App<Props> {
  static getInitialProps = async function ({Component, router, ctx}) {
    let pageProps = {};

    if (Component.getInitialProps) {
      pageProps = await Component.getInitialProps(ctx);
    }

    const {req} = ctx;
    const {locale, messages} = req || (window as any).__NEXT_DATA__.props;
    return {pageProps, locale, messages};
  };


  render() {
    const {Component, pageProps, locale, messages} = this.props;
    return (
      <ThemeProvider theme={theme}>
        <IntlProvider locale={locale} messages={messages}>
          <Component {...pageProps} />
        </IntlProvider>
      </ThemeProvider>
    )
  }
}

扩展App组件,相当于全局处理,所以无论你访问哪个页面,都会先处理这里的逻辑。getInitialProps方法中,从上下文ctx中拿到req,从req中获得我们在后端插入的locale和messages属性,将这两个属性设置给<IntlProvider/>组件。使用多语言如下:

// index.tsx
export default class Index extends React.Component {
  render() {
    return (
        <div>
          <FormattedMessage id="greeting" defaultMessage="拉拉拉"/>
        </div>
    )
  }
}

使用FormattedMessage组件根据id属性来加载多语言内容,defaultMessage用于在找不到对应id的时候默认展示。

切换多语言

使用Redux

官方范例

Redux的基础内容不在这里赘述,主要说明一下和Next.js服务端部分结合使用。组件和服务端沟通的桥梁是getInitialProps方法,所以在这里将状态树构造出来,挂到上下文中。为了实现这个过程,装饰一下_app.tsx

const isServer = typeof window === 'undefined';
const __NEXT_REDUX_STORE__ = '__NEXT_REDUX_STORE__';

function getOrCreateStore(initialState) {
  // 服务端总是新建状态树
  if (isServer) {
    return initializeStore(initialState)
  }

  // 客户端的话就要考虑一下是不是已有全局状态树
  if (!window[__NEXT_REDUX_STORE__]) {
    // 状态树会被挂到window下的全局属性
    window[__NEXT_REDUX_STORE__] = initializeStore(initialState)
  }
  return window[__NEXT_REDUX_STORE__]
}

export default App => {
  return class AppWithRedux extends React.Component {
    static async getInitialProps(appContext) {
      const reduxStore = getOrCreateStore();

      // 服务端初始化的store被挂到上下文中
      appContext.ctx.reduxStore = reduxStore;

      let appProps = {};
      if (typeof App.getInitialProps === 'function') {
        appProps = await App.getInitialProps(appContext)
      }

      return {
        ...appProps,
        initialReduxState: reduxStore.getState()
      }
    }

    constructor(props) {
      super(props);
      // 客户端也去初始化,由于区分了端,这里保证拿到的状态树是统一的
      this.reduxStore = getOrCreateStore(props.initialReduxState)
    }
  }
}

开发便捷性

使用别名

为了编辑器和next都能够正常识别使用别名,我们需要ts、next内置webpack和编辑器支持。ts支持需要在tsconfig.json文件中对compilerOptions属性进行扩展配置,一般来说可以把这部分隔离出来,在tsconfig.json中用extends进行融合:

// path.json
{"compilerOptions": {
    "baseUrl": ".",
    "paths": {
        "@pageComponents/*": ["./components/pages/*"],
    }
}}
// tsconfig.json
{
    "extends": "./paths.json",
    // ... 其他配置内容
}

next内置的webpack支持别名就比较简单了,webpack有对应的设置项:

// next.config.js
const path = require('path');
module.exports = {
    // ...其他next配置
    webpack: (config, {isServer}) => {
        // .. 其他webpack配置
        Object.assign(
            config.resolve.alias, 
            {'@pageComponents': path.resolve(__dirname, 'components/pages')});
        return config
    },
}

使用webstorm编辑器的话,为了能够使用快捷跳转,还要给编辑器提供一个单独的config文件用于创建索引,这个文件应当和普通webpack的配置文件写法相同,只提供alias的部分就行,然后在webstorm的项目配置中选择这个文件即可:

// alias.config.js
const path = require('path');
module.exports = {
    resolve: {
        alias: {
            '@pageComponents': path.resolve(__dirname, 'components/pages')
        }
    }
};

当前页面是本站的「Baidu MIP」版。查看和发表评论请点击:完整版 »