[ Next.js实战项目 | 青训营笔记]

400 阅读7分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 13 天

一、本堂课重点内容:

  • 什么是Next.js
  • Next.js客户端开发
  • Next.js服务端开发
  • 核心功能讲解

二、详细知识点介绍:

1. 什么是Next.js

Next.js是开发C端应用的框架。B端应用是挂在内网上,提供给管理员使用,如学生管理系统等,C端应用则是挂在外网上,提供给整个互联网上的用户使用。

Next.js是一个构建于Node.js之上的开源Web开发框架,支持基于React的Web应用程序功能,例如服务端渲染和生成静态网站。

优点:上手快,能力集全,而且覆盖了足够多的性能优化和生态。对于新同学掌握前后端一体化的开发模式很友好。

2. Next.js客户端开发

初始化:npx create-next-app@latest --typescript

CMS仓库地址:github.com/czm12904337…

Demo仓库地址:github.com/czm12904337…

next-env.d.ts用于确保TypeScript编译器选择Next.js类型,可以放到.gitignore中,不需要变更。 next.config.js保存着nextjs的配置,可以补充webpack的一些配置,例如补充别名等。

  • 数据注入方式:
  1. CSR请求数据(客户端数据注入):useEffect钩子
useEffect(() => {
    axios.get ...(() => {
        // 传递数据
    })
}, [])

在SSR和SSG中用这种方式,与CSR的最直接区别在于无法在network查询到请求接口,因此一般不这么使用。

  1. getInitialProps

在服务器端执行,只能在页面层面进行绑定,采用同构,首次渲染服务器端渲染,路由跳转使用客户端路由。意味着如果使用router跳转当前页,会在客户端执行这部分逻辑。

这个api是走在服务器端的注水过程,因为需要优化SEO,这个请求必须在访问掘金这个路由之前就注入HTML流中,如果走在客户端,是无法获取这些数据的。但是如果存在一些内部跳转,他就会走客户端的路由;而跳转后刷新页面,即直接访问这个页面,就会走服务器端路由,来保证SEO优化直接请求可以拿到所有数据。

对于初学者来说,很容易弄混,于是拆分为getServerSideProps和getStaticProps。

Demo中pages\article[articleId].tsx:

Article.getInitialProps = async (context): Promise<IArticleProps> => {
  // debugger;         //加入debugger后,内部跳转触发,跳转后刷新页面不触发
  const { articleId } = context.query;
  const { data } = await axios.get(`${LOCALDOMAIN}/api/articleInfo`, {
    params: {
      articleId,
    },
  });
  return data;
};

  1. getServerSideProps

SSR,一定会走服务端,与getInitialProps不同的是即使用route跳转当前页,也只会在服务端执行这部分逻辑。即内部跳转也会在服务器端执行。

与getInitialProps效果非常相似,但返回data时要注意用props包裹。

Demo中pages\article[articleId].tsx:

export const getServerSideProps: GetServerSideProps = async context => {
  // debugger;                //加入debugger后,无论如何也不触发
  const { articleId } = context.query;
  const { data } = await axios.get(`${LOCALDOMAIN}/api/articleInfo`, {
    params: {
      articleId,
    },
  });
  return {
    props: data, // 需要拿props包裹
  };
};
  1. getStaticProps

SSG,在服务器端构建时执行,如果设计动态路由(带参数),需要使用getStaticPaths配置所有可能的参数情况。

如果说SSR是点菜现做,SSG就是自助餐,厨师做好了菜,客户自己拿。

getStaticPaths的作用是把所有可能性都列出来,然后把这些可能性都存到CDN中,然后再去访问。但是这种方法还是比较呆,所以建议在比较固定且可能性较少的情况下使用。

Demo中pages\article[articleId].tsx:

ssg;
export const getStaticPaths: GetStaticPaths = async () => ({
  paths: [{ params: { articleId: '1' } }],
  fallback: false,
});

export const getStaticProps: GetStaticProps = async context => {
  const { articleId } = context.params as any;
  const { data } = await axios.get(`${LOCALDOMAIN}/api/articleInfo`, {
    params: {
      articleId,
    },
  });
  return {
    props: data,
  };
};
3. Next.js服务端开发
  • BFF层开发

BFF层不生产数据,只搬运数据

服务端开发是api开发部分,即BFF层开发,和Express等开发类似,区别是并没有参数可以直接区别请求类型。

没有中间件,无论是GET还是POST都可以请求,如果需要添加限制,可以取出request.method进行判断。

  • Strapi - headless CMS

CMS层则负责数据的生产

CMS:一个后台管理平台。一个网站的内容不可能一直不更新,但是如果只是修改一下文案,其实不需要研发人员来修改,因此需要一个数据管理平台来维护整个网站上的数据,相当于给运营团队使用的数据库。

Strapi是一个开源项目,可以帮助我们快速地搭建一个CMS平台

Strapi仓库:github.com/strapi/stra…

初始化:npx create-strapi-app my-project --quickstart

创建完打开就会进入网站,就可以进行接口的创建。

一个接口的生成有以下几个过程:

1.content-type builder编辑结构体

2.content manager配置数据源,并且发布

3.setting roles里选择对应角色并勾选要发布的接口类型

4.如果涉及嵌套,要在接口后加上populate=deep参数(需要安装依赖npm install strapi-plugin-populate-deep --save),没安装加参数populate=*,但只能嵌套一层。

如果没有加populate=deep参数,就只会展示基础数据,不会把结构体中的层级遍历出来。

比较重要的是Relation结构体,表示不同的结构体之间存在关联,比如某个结构体是另一个结构体的子结构体,

  • 调试方式

在代码中添加debugger,使用JavaScript Debug Terminal进行调试,这个终端是服务器端的控制台。然后在这个终端执行npm run dev,就可以运行并触发断点。

如果希望使用浏览器控制台进行调试,可以在终端执行npm run debugger,这是next.js自己提供的命令。运行后出现的控制台是客户端的控制台,而点击这个控制台页面左上角的Nodejs标志,可以打开服务端的控制台。

4. 核心功能讲解
  • 首页功能实现
  1. 首页 & 动画 & 多媒体适配

  2. BFF

  3. Strapi

Strapi唯一需要注意的一点是需要populate=deep才能显示子结构体。

  • 文章页实现
  1. 首页 & 动画 & 多媒体适配

  2. BFF

  3. Strapi分页(/api/articles?pagination[page]=1&pagination[pageSize]=10 //按10个/页分页,返回第一页的数据)

把pageNo和pageSize作为query存入并处理,就可以返回对应数据。

CMS中src\api\article-introduction\controllers\article-introduction.js:

module.exports = createCoreController(
  "api::article-introduction.article-introduction",
  ({ strapi }) => ({
    async find(ctx) {
      const { pageNo, pageSize, ...params } = ctx.query;
      if (pageNo && pageSize) {
        ctx.query = {
          ...params,
          "pagination[page]": Number(pageNo),
          "pagination[": Number(pageSize),
        };
      }
      const { data, meta } = await super.find(ctx);
      return { data: removeAttrsAndId(removeTime(data)), meta };
    },
  })
);
  1. 多媒体格式的转换

-markdown 转 html:npm install showdown --save

-html转dom:dangerouslySetlnnerHTML

-公共样式的定义

Demo中pages\article[articleId].tsx:

const Article: NextPage<IArticleProps> = ({ title, author, description, createTime, content }) => {
  const converter = new showdown.Converter();
  return (
    <div className={styles.article}>
      <h1 className={styles.title}>{title}</h1>
      <div className={styles.info}>
        作者:{author} | 创建时间: {createTime}
      </div>
      <div className={styles.description}>{description}</div>
      <div dangerouslySetInnerHTML={{ __html: converter.makeHtml(content) }} className={styles.content} />
    </div>
  );
};

showdown.Converter()把markdown转成html,再用dangerouslySetInnerHTML={{ __html: converter.makeHtml(content) }} 转化成dom,就可以给他设置className属性了。

  • 主题化功能实现

1 .基础样式和背景的抽离

  1. 主题化context全局注入

  2. 从注入数据中取出theme和setTheme

  3. 多进程间的主题同步

Demo中stores\theme.tsx:


export const ThemeContext = createContext<IThemeContextProps>({} as IThemeContextProps);

export const ThemeContextProvider = ({ children }: IProps): JSX.Element => {
  const [theme, setTheme] = useState<Themes>(Themes.light);

  // 监听本地缓存来同步不同页面间的主题
  useEffect(() => {
    const checkTheme = (): void => {
      const item = (localStorage.getItem('theme') as Themes) || Themes.light;
      setTheme(item);
      document.getElementsByTagName('html')[0].dataset.theme = item;
    };
    checkTheme();
    window.addEventListener('storage', checkTheme);
    return (): void => {
      window.removeEventListener('storage', checkTheme);
    };
  }, []);

  return (
    <ThemeContext.Provider
      value={{
        theme,
        setTheme: (currentTheme): void => {
          setTheme(currentTheme);
          localStorage.setItem('theme', currentTheme);
          document.getElementsByTagName('html')[0].dataset.theme = currentTheme;
        },
      }}
    >
      {children}
    </ThemeContext.Provider>
  );
};

document.getElementsByTagName('html')[0].dataset.theme = currentTheme;在html流中加入了当前属性。

设置全局scss(global.scss)来控制全局的属性值,在使用时只需要调用即可。

多进程同步:设置不同进程下,页面样式一致。window.addEventListener('storage', checkTheme);监听storage变化,则改变当前页面主题。

将theme和useAgent等逻辑放在单独的文件中,更易于维护。

  1. http://localhost:3000http://127.0.0.1:3000 的主题无法共享

因为这个使用的是localStorage进行共享,同一个域名可以共用一个localStorage,但是这两个东西是跨域的,所以并不同源,无法共享

三、课后个人总结:

通过本节课程,我明白了Next.js的原理和优点;了解到Next.js的客户端开发方式,包括四种数据注入方式的区别和实现;并且理解了Next.js的客户端开发方式,包括BFF层开发和CMS层开发和客户端与服务端的调试方式;还了解到了首页、文章页和主题化功能的实现。