使用 Next.js 搭建一个门户网站

avatar
前端经理 @阿巴阿巴

我正在参加「掘金·启航计划」

最近在开发公司的门户网站,由于技术栈是 React,所以就用了 Next.js 来开发。最终项目的目录结构如下:

image.png

下面记述一下开发过程。

1、脚手架创建

使用命令行创建:

yarn create next-app --typescript

创建完成后,运行yarn dev就可在localhost:3000看到界面。

2、整体布局与路由

next自带一个pages页面路由,不用单独再配置路由了,这里展示一下我的页面:

image.png

对应的是门户网站导航栏的内容:

image.png

目录介绍:

  • index:首页入口
  • products:产品
  • api:导航
  • about:关于我们

整个项目入口文件为 index.tsx:

import Head from 'next/head'

return (
    <main className="container">
      {/* 页面滚动指示器 */}
      <section className={`indicator scroll-indicator`}></section>

      {/* 页面 tab,可添加 link、meta标签做 SEO等 */}
      <Head>
        ...
      </Head>

      {/* 顶部菜单栏 */}
      <Header style={headerStyle} />

      {/* 轮播图 */}
      <Banner />

      {/* 产品与服务 */}
      <section className="product common-box">
        <div className="product-container">
          <Product />
        </div>
      </section>

      {/* 核心优势 */}
      <section className="product common-box">
        <div className="wrapper-container">
          <div className="product-title">
            <h3 className="common-title">我们的优势</h3>
            <span className="common-sub-title">云端互联 融合智能 无限扩展 即时响应</span>
          </div>
          <div className="product-container">
            <Advantage />
          </div>
        </div>
      </section>

      {/* 应用场景 */}
      <section className="product common-box product-scene">
        <div className="wrapper-container scene-container">
          <div className="product-title">
            <h3 className="common-title">应用场景</h3>
          </div>
          <Scene />
        </div>
      </section>

      {/* 合作伙伴 */}
      <section className="product common-box" id="numberWrap">
        <div className="wrapper-container">
          <div className="product-title">
            <h3 className="common-title">更多用户的选择</h3>
            <span className="common-sub-title">
              优质的服务,一对一的运营关注、让XXX获得更多用户的认可
            </span>
          </div>
          
          ...
        </div>
      </section>

      {/* 合作伙伴商标墙 */}
      <WallImage />

      {/* 认证展示 */}
      <Authentication />

      {/* 固定位置的按钮 */}
      <FixTag showGoTop={headerStyle.showGoTop} />
      
      {/* 底部企业信息 */}
      <Footer />
    </main>
  );

上面展示了整个门户的布局。在布局中,尽量使用 h5 有意义的标签,比如 main、section 等,便于搜索引擎识别。接下来看一下路由如何跳转:

// next自带有link
import Link from 'next/link';

// 根据 pages 文件夹层级进行设置
<Link href={'/products/xxx'}>
  ...
</Link>

当然也可使用 API 来跳转:

import { useRouter } from 'next/router';

const router = useRouter();
router.push('/products/xxx');

3、图片加载

next依然提供了图片加载组件:

import Image from 'next/image';

<Image width="180" height="34" src={'/web/images/authentication/auth.png'} alt="UMCloud" />

为节省首页加载时间,图片路径不建议使用本地的,这里需要部署静态资源服务器,src直接获取即可。配置静态资源获取时,可以在配置文件中(next.config.js)统一拦截请求:

async rewrites() {
    return {
      fallback: [
        {
          source: '/web/images/:image*',
          destination: `https://xxx.fileos.com/web/images/:image*`
        }
      ],
    }
},

当然了,配置文件中还可以针对远程服务器进行配置,用于允许对远程图片使用内部自建的优化策略,自动识别和调整图片宽高:

// next.config.js
images: {
    loader: 'imgix',
    domains: ['xxx.fileos.com'],
    path: 'https://xxx.fileos.com/'
},

其中自建优化的 loader 有如下几个(官网文档):

  • Vercel: Works automatically when you deploy on Vercel, no configuration necessary. Learn more
  • Imgixloader: 'imgix'
  • Cloudinaryloader: 'cloudinary'
  • Akamailoader: 'akamai'
  • Custom: loader: 'custom' use a custom cloud provider by implementing the loader prop on the next/image component

这些loader都是 next 合作的云厂商提供的,详细可点击查看。

4、使用 css module

next推荐使用 css module 来样式隔离。使用如下:

import styles from './index.module.css'

<section className={styles["section-culture"]}>
 ...
</section>

此时,直接定义 className 将无效。

5、小组件

header

头部布局采用左中右布局,左边是一个 logo 图片, 中间是菜单项,右边是操作按钮:

return (
    <header className={headerStyle['header']} style={rest}>
        <div className={headerStyle['header-nav']}>
            <Link href="/">
               // logo
            </Link>
            
            <ul className={headerStyle['header-menu']}>
                {Menus.map((menu, index) => (
                    // 产品的下拉菜单
                ))}
            </ul>
            
            <nav className={`um-fr ${headerStyle['header-options']}`}>
                <span
                    onClick={gotoLogin}
                    title="登录"
                    className={`btn ${headerStyle['btn-login']}`}
                >
                    登录
                </span>
            </nav>
            </div>
    </header>
);

并且需要设置header样式的背景色为透明的,默认 position 为 sticky,在普通情况下是绝对定位,在页面滚动时变为固定定位。并且需要监听滚动区域变化,在头部组件完全消失,变为固定定位时,背景色应该变为白色,并设置 border-shadow:

顶部:

image.png

滚动后:

image.png

banner

使用 Swiper 插件做轮播图,可以参考我的这篇文章:Swiper 轮播图实现动态进度条

具体实现方式可以参考官方文档:Swiper8

合作伙伴商标墙

其原理还是使用 Swiper,只不过是竖着的,并且隐藏了分页器:

<Swiper
    spaceBetween={20}
    speed={2000}
    slidesPerView={3}
    allowTouchMove={false}
    style={{
        height: '285px',
    }}
    loop
    // 竖着滚动
    direction="vertical"
    // 间隔2s
    autoplay={{
        disableOnInteraction: false,
        delay: 2000
    }}
    // 自动播放
    modules={[Autoplay]}
>
    {wallList.map((wall, index) => {
        return (
            <SwiperSlide key={index}>
               // 图片 item
            </SwiperSlide>
        );
    })}
</Swiper>

效果图(商标是假的):

image.png

6、部署

区分开发和线上环境

在 package.json 里配置:

 "scripts": {
    "dev": "cross-env NEXT_PUBLIC_DOMAIN_ENV=development next dev",
    "build": "cross-env NEXT_PUBLIC_DOMAIN_ENV=production next build --profile",
    "build:test": "cross-env NEXT_PUBLIC_DOMAIN_ENV=development next build --debug"
 }

如此,便有了一个全局环境变量了(next自带的配置不支持动态变量)。

使用:

export const getEnvParams = () => {
  const Env = process.env.NEXT_PUBLIC_DOMAIN_ENV === 'production' ? 'PROD' : 'DEV';

  const params =  {
    'DEV':  {
      login: 'https://xxx.com/'
    },
    'PROD': {
      login: 'https://yyy.com/',
    }
  }

  return params[Env];
}

部署

这一块内容有点多,我是做了CI/CD的,其实可以简单一点,门户网站不会有频繁的版本更新和修改,可以使用 nginx 部署。

运行:

yarn build

然后在 .next 文件夹下就会看到build完后的内容,之后运行:

yarn start

就可以将打包的内容在3000端口启动起来。将其托管在远程服务器即可。


如果使用 CI自动化部署,需要新建 .gitlab-ci.yml 的配置文件,前提要配置好 gitlab 的 runner。

定义管道流程:

stages:
  - check
  - build
  - deploy
  1. 代码检查:
code-check:
  stage: check
  script:
    - ls -al
    - echo >> /etc/hosts '10.xx.xxx.xxx registry.npm.pre.xxxx.com'
    - yarn config set registry http://registry.npm.taobao.org/
    - yarn add global eslint typescript @babel/preset-env @babel/preset-react @babel/preset-typescript @babel/core @babel/plugin-transform-runtime @typescript-eslint/parser @babel/plugin-proposal-class-properties @babel/plugin-proposal-optional-chaining @babel/plugin-syntax-dynamic-import
    - mkdir .tmp
    - touch .tmp/.eslintrc
    - echo '{"parser":"@typescript-eslint/parser","parserOptions":{"ecmaVersion":7,"sourceType":"module"}}' >> .tmp/.eslintrc
    - ./node_modules/.bin/eslint "src/**/*.tsx" "src/**/*.js" -c .tmp/.eslintrc --no-error-on-unmatched-pattern --no-eslintrc --fix-dry-run
  tags:
    - uaek-x6
  cache:
    key:
      files:
        - .tmp/.eslintrc
      prefix: $CI_JOB_NAME
    paths:
      - node_modules/
  only:
    refs:
      - feature/test

在这里设置触发分支为 test,脚本使用 eslint 进行检查。

  1. 构建阶段:
node-build-pre:
  stage: build
  image: hub.xxxx.com/xxx/xxx-yyyy-executor:latest
  script:
    - IMAGE_TAG=latest
    - cat $CI_PROJECT_DIR/Dockerfile 
    - echo >> /etc/hosts '2002:a40:b4:1::1 hub-test.service.xxxx.cn'
    - echo $IMAGE_NAME_CONSOLE_CLI_TEST03:$IMAGE_TAG $KUN_IMAGE_PUSH_SECRET_NAME_X6
    - /kaniko/executor -c $CI_PROJECT_DIR -f $CI_PROJECT_DIR/Dockerfile -d $IMAGE_NAME_CONSOLE_CLI_TEST03:$IMAGE_TAG
  cache:
    key:
      files:
        - package-lock.json
        - package.json
      prefix: $CI_PROJECT_NAME-$CI_JOB_NAME
    paths:
      - node_modules/
  tags:
     - uaek-x6
  only:
    refs:
      - feature/test

这里使用了公司docker镜像,在镜像文件里会执行 yarn build 等操作。(next build指令会自动将env.NODE_ENV 设置为 production)

image.png

  1. 部署
pre-node-deploy:
  extends: 
    - .node-deploy-pre
  stage: deploy
  when: manual
  only:
    refs:
      - feature/test

设置了触发分支和手动触发模式。

.node-deploy-pre:
  variables:
    DEPLOY_ENV: $PRE_HOME
    STARK_ENV: 'pre'
    REMOTE_PATH: '/data/deploy'
  script:
    - ssh root@$PRE_HOME -tt << remotessh
    - cd $REMOTE_PATH
    - sh deploy.sh
    - exit
    - remotessh
  dependencies:
    - node-build-pre
  tags:
    - uaek-x6

部署就简单了,ssh进入远程服务器,执行一个脚本 deploy.sh,其实就是拉取刚刚 build 好的镜像。