我正在参加「掘金·启航计划」
最近在开发公司的门户网站,由于技术栈是 React,所以就用了 Next.js 来开发。最终项目的目录结构如下:
下面记述一下开发过程。
1、脚手架创建
使用命令行创建:
yarn create next-app --typescript
创建完成后,运行yarn dev
就可在localhost:3000
看到界面。
2、整体布局与路由
next自带一个pages页面路由,不用单独再配置路由了,这里展示一下我的页面:
对应的是门户网站导航栏的内容:
目录介绍:
- 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
- Imgix:
loader: 'imgix'
- Cloudinary:
loader: 'cloudinary'
- Akamai:
loader: 'akamai'
- Custom:
loader: 'custom'
use a custom cloud provider by implementing theloader
prop on thenext/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:
顶部:
滚动后:
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>
效果图(商标是假的):
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
- 代码检查:
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 进行检查。
- 构建阶段:
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)
- 部署
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 好的镜像。