前言

老大说以后会用 next 来做一下 SSR 的项目,让我们有空先学学。又从 0 开始学习新的东西了,想着还是记录一下学习历程,有输入就要有输出吧,免得以后给忘记学了些什么~


Next框架与主流工具的整合

github地址:https://github.com/code-coder/next-mobile-complete-app

首先,clone Next.js 项目,学习里面的templates。
打开一看,我都惊呆了,差不多有150个搭配工具个template,有点眼花缭乱。
这时候就需要明确一下我们要用哪些主流的工具了:
  • ️ 数据层:redux + saga
  • ️ 视图层:sass + postcss
  • ️ 服务端:koa

做一个项目就像造一所房子,最开始就是“打地基”:

1. 新建了一个项目,用的是这里面的一个with-redux-saga的template 戳这里

2. 添加sass和postcss,参考的是 这里

  • 新建next.config.js,复制以下代码:

const withSass = require('@zeit/next-sass');
module.exports = withSass({
postcssLoaderOptions: {
parser: true,
config: {
ctx: {
theme: JSON.stringify(process.env.REACT_APP_THEME)
}
}
}
});
  • 新建postcss.config.js,复制以下代码:

module.exports = {
plugins: {
autoprefixer: {}
}
};
  • package.js添加自定义browserList,这个就根据需求来设置了,这里主要是移动端的。

// package.json
"browserslist": [
"IOS >= 8",
"Android > 4.4"
],
  • 顺便说一下browserlist某些配置会报错,比如直接填上默认配置

"browserslist": [
"last 1 version",
"> 1%",
"maintained node versions",
"not dead"
]
// 会报以下错误
Unknown error from PostCSS plugin. Your current PostCSS version is 6.0.23, but autoprefixer uses 5.2.18. Perhaps this is the source of the error below.

3. 配置koa,参照custom-server-koa

  • 新建server.js文件,复制以下代码:

const Koa = require('koa');
const next = require('next');
const Router = require('koa-router'); const port = parseInt(process.env.PORT, 10) || 3000;
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 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

"scripts": {
"dev": "node server.js",
"build": "next build",
"start": "NODE_ENV=production node server.js"
}

现在只是把地基打好了,接着需要完成排水管道、钢筋架构等铺设:

  • ️ 调整项目结构
  • ️ layout布局设计
  • ️ 请求拦截、loading状态及错误处理

1. 调整后的项目结构


-- components
-- pages
++ server
|| -- server.js
-- static
++ store
|| ++ actions
|| -- index.js
|| ++ reducers
|| -- index.js
|| ++ sagas
|| -- index.js
-- styles
-- next.config.js
-- package.json
-- postcss.config.js
-- README.md

2. layout布局设计。

ant design 是我使用过而且比较有好感的UI框架。既然这是移动端的项目,ant design mobile 成了首选的框架。我也看了其他的主流UI框架,现在流行的UI框架有Amaze UIMint UIFrozen UI等等,个人还是比较喜欢ant出品的。

恰好templates中有ant design mobile的demo:with-ant-design-mobile

  • 基于上面的项目结构整合with-ant-design-mobile这个demo。
  • 新增babel的配置文件:.babelrc 添加以下代码:

{
"presets": ["next/babel"],
"plugins": [
[
"import",
{
"libraryName": "antd-mobile"
}
]
]
}
  • 修改next.config.js为:

const withSass = require('@zeit/next-sass');
const path = require('path');
const fs = require('fs');
const requireHacker = require('require-hacker'); function setupRequireHacker() {
const webjs = '.web.js';
const webModules = ['antd-mobile', 'rmc-picker'].map(m => path.join('node_modules', m)); requireHacker.hook('js', filename => {
if (filename.endsWith(webjs) || webModules.every(p => !filename.includes(p))) return;
const webFilename = filename.replace(/\.js$/, webjs);
if (!fs.existsSync(webFilename)) return;
return fs.readFileSync(webFilename, { encoding: 'utf8' });
}); requireHacker.hook('svg', filename => {
return requireHacker.to_javascript_module_source(`#${path.parse(filename).name}`);
});
} setupRequireHacker(); function moduleDir(m) {
return path.dirname(require.resolve(`${m}/package.json`));
} module.exports = withSass({
webpack: (config, { dev }) => {
config.resolve.extensions = ['.web.js', '.js', '.json']; config.module.rules.push(
{
test: /\.(svg)$/i,
loader: 'emit-file-loader',
options: {
name: 'dist/[path][name].[ext]'
},
include: [moduleDir('antd-mobile'), __dirname]
},
{
test: /\.(svg)$/i,
loader: 'svg-sprite-loader',
include: [moduleDir('antd-mobile'), __dirname]
}
);
return config;
}
});
  • static新增rem.js

(function(doc, win) {
var docEl = doc.documentElement,
// isIOS = navigator.userAgent.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/),
// dpr = isIOS ? Math.min(win.devicePixelRatio, 3) : 1;
// dpr = window.top === window.self ? dpr : 1; //被iframe引用时,禁止缩放
dpr = 1;
var scale = 1 / dpr,
resizeEvt = 'orientationchange' in window ? 'orientationchange' : 'resize';
docEl.dataset.dpr = dpr;
var metaEl = doc.createElement('meta');
metaEl.name = 'viewport';
metaEl.content =
'initial-scale=' + scale + ',maximum-scale=' + scale + ', minimum-scale=' + scale + ',user-scalable=no';
docEl.firstElementChild.appendChild(metaEl);
var recalc = function() {
var width = docEl.clientWidth;
// 大于1280按1280来算
if (width / dpr > 1280) {
width = 1280 * dpr;
}
// 乘以100,px : rem = 100 : 1
docEl.style.fontSize = 100 * (width / 375) + 'px';
doc.body &&
doc.body.style.height !== docEl.clientHeight &&
docEl.clientHeight > 360 &&
(doc.body.style.height = docEl.clientHeight + 'px');
};
recalc(); if (!doc.addEventListener) return;
win.addEventListener(resizeEvt, recalc, false);
win.onload = () => {
doc.body.style.height = docEl.clientHeight + 'px';
};
})(document, window);
  • 增加移动端设备及微信浏览器的判断

(function() {
// 判断移动PC端浏览器和微信端浏览器
var ua = navigator.userAgent;
// var ipad = ua.match(/(iPad).* OS\s([\d _] +)/);
var isAndroid = ua.indexOf('Android') > -1 || ua.indexOf('Adr') > -1; // android
var isIOS = !!ua.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/); // ios
if (/(iPhone|iPad|iPod|iOS|Android)/i.test(navigator.userAgent)) {
window.isAndroid = isAndroid;
window.isIOS = isIOS;
window.isMobile = true;
} else {
// 电脑PC端判断
window.isDeskTop = true;
}
ua = window.navigator.userAgent.toLowerCase();
if (ua.match(/MicroMessenger/i) == 'micromessenger') {
window.isWeChatBrowser = true;
}
})();
  • _document.js新增引用

```<Head>
<script src="/static/rem.js" />
<script src="/static/user-agent.js" />
<link rel="stylesheet" type="text/css" href="//unpkg.com/antd-mobile/dist/antd-mobile.min.css" />
</Head>
```

  • 构造布局
  1. 在components文件夹新增layouttabs文件夹

++ components
|| ++ layout
|| || -- Layout.js
|| || -- NavBar.js
|| ++ tabs
|| || -- TabHome.js
|| || -- TabIcon.js
|| || -- TabTrick.js
|| || -- Tabs.js
  1. 应用页面大致结构是(意思一下)
  • 首页
nav
content
tabs
  • 其他页
nav
content
  • 最后,使用redux管理nav的title,使用router管理后退的箭头

// other.js
static getInitialProps({ ctx }) {
const { store, req } = ctx;
// 通过这个action改变导航栏的标题
store.dispatch(setNav({ navTitle: 'Other' }));
const language = req ? req.headers['accept-language'] : navigator.language; return {
language
};
}

// NavBar.js
componentDidMount() {
// 通过监听route事件,判断是否显示返回箭头
Router.router.events.on('routeChangeComplete', this.handleRouteChange);
} handleRouteChange = url =&gt; {
if (window &amp;&amp; window.history.length &gt; 0) {
!this.setState.canGoBack &amp;&amp; this.setState({ canGoBack: true });
} else {
this.setState.canGoBack &amp;&amp; this.setState({ canGoBack: false });
}
};

// NavBar.js
let onLeftClick = () =&gt; {
if (this.state.canGoBack) {
// 返回上级页面
window.history.back();
}
};

3、请求拦截、loading及错误处理

  • 封装fetch请求,使用单例模式对请求增加全局loading等处理。

要点:1、单例模式。2、延迟loading。3、server端渲染时不能加载loading,因为loading是通过document对象操作的


import { Toast } from 'antd-mobile';
import 'isomorphic-unfetch';
import Router from 'next/router'; // 请求超时时间设置
const REQUEST_TIEM_OUT = 10 * 1000;
// loading延迟时间设置
const LOADING_TIME_OUT = 1000; class ProxyFetch {
constructor() {
this.fetchInstance = null;
this.headers = { 'Content-Type': 'application/json' };
this.init = { credentials: 'include', mode: 'cors' };
// 处理loading
this.requestCount = 0;
this.isLoading = false;
this.loadingTimer = null;
} /**
* 请求1s内没有响应显示loading
*/
showLoading() {
if (this.requestCount === 0) {
this.loadingTimer = setTimeout(() =&gt; {
Toast.loading('加载中...', 0);
this.isLoading = true;
this.loadingTimer = null;
}, LOADING_TIME_OUT);
}
this.requestCount++;
} hideLoading() {
this.requestCount--;
if (this.requestCount === 0) {
if (this.loadingTimer) {
clearTimeout(this.loadingTimer);
this.loadingTimer = null;
}
if (this.isLoading) {
this.isLoading = false;
Toast.hide();
}
}
} /**
* 获取proxyFetch单例对象
*/
static getInstance() {
if (!this.fetchInstance) {
this.fetchInstance = new ProxyFetch();
}
return this.fetchInstance;
} /**
* get请求
* @param {String} url
* @param {Object} params
* @param {Object} settings: { isServer, noLoading, cookies }
*/
async get(url, params = {}, settings = {}) {
const options = { method: 'GET' };
if (params) {
let paramsArray = [];
// encodeURIComponent
Object.keys(params).forEach(key =&gt; {
if (params[key] instanceof Array) {
const value = params[key].map(item =&gt; '"' + item + '"');
paramsArray.push(key + '=[' + value.join(',') + ']');
} else {
paramsArray.push(key + '=' + params[key]);
}
});
if (url.search(/\?/) === -1) {
url += '?' + paramsArray.join('&amp;');
} else {
url += '&amp;' + paramsArray.join('&amp;');
}
}
return await this.dofetch(url, options, settings);
} /**
* post请求
* @param {String} url
* @param {Object} params
* @param {Object} settings: { isServer, noLoading, cookies }
*/
async post(url, params = {}, settings = {}) {
const options = { method: 'POST' };
options.body = JSON.stringify(params);
return await this.dofetch(url, options, settings);
} /**
* fetch主函数
* @param {*} url
* @param {*} options
* @param {Object} settings: { isServer, noLoading, cookies }
*/
dofetch(url, options, settings = {}) {
const { isServer, noLoading, cookies = {} } = settings;
let loginCondition = false;
if (isServer) {
this.headers.cookies = 'cookie_name=' + cookies['cookie_name'];
}
if (!isServer &amp;&amp; !noLoading) {
loginCondition = Router.route.indexOf('/login') === -1;
this.showLoading();
}
const prefix = isServer ? process.env.BACKEND_URL_SERVER_SIDE : process.env.BACKEND_URL;
return Promise.race([
fetch(prefix + url, { headers: this.headers, ...this.init, ...options }),
new Promise((resolve, reject) =&gt; {
setTimeout(() =&gt; reject(new Error('request timeout')), REQUEST_TIEM_OUT);
})
])
.then(response =&gt; {
!isServer &amp;&amp; !noLoading &amp;&amp; this.hideLoading();
if (response.status === 500) {
throw new Error('服务器内部错误');
} else if (response.status === 404) {
throw new Error('请求地址未找到');
} else if (response.status === 401) {
if (loginCondition) {
Router.push('/login?directBack=true');
}
throw new Error('请先登录');
} else if (response.status === 400) {
throw new Error('请求参数错误');
} else if (response.status === 204) {
return { success: true };
} else {
return response &amp;&amp; response.json();
}
})
.catch(e =&gt; {
if (!isServer &amp;&amp; !noLoading) {
this.hideLoading();
Toast.info(e.message);
}
return { success: false, statusText: e.message };
});
}
} export default ProxyFetch.getInstance();

写在最后

一个完整项目的雏形大致出来了,但是还是需要在实践中不断打磨和优化。

如有错误和问题欢迎各位大佬不吝赐教 :)

来源:https://segmentfault.com/a/1190000016383263

Next轻量级框架与主流工具的整合的更多相关文章

  1. Java EE互联网轻量级框架整合开发— SSM框架(中文版带书签)、原书代码

    Java EE互联网轻量级框架整合开发 第1部分 入门和技术基础 第1章 认识SSM框架和Redis 2 1.1 Spring框架 2 1.2 MyBatis简介 6 1.3 Spring MVC简介 ...

  2. 互联网轻量级框架SSM-查缺补漏第一天

    简言:工欲其事必先利其器,作为一个大四的准毕业生,在实习期准备抽空补一下基础.SSM框架作为互联网的主流框架,在会使用的基础上还要了解其原理,我觉得会对未来的职场会有帮助的.我特意的买了一本<J ...

  3. 互联网轻量级框架SSM-查缺补漏第八天(MyBatis插件plugin使用及原理)

    简言:今天进行第八天的记录(只是写了八天).有的时候看的多,有的时候看的少,看的少的时候就攒几天一起写了.而今天这个插件我昨天写了一下午,下班没写完就回去了,今天把尾收了,再加上一个过程图方便下面原理 ...

  4. 前端Js框架汇总(工具多看)

    前端Js框架汇总(工具多看) 一.总结 一句话总结: 二.前端Js框架汇总 概述: 有些日子没有正襟危坐写博客了,互联网飞速发展的时代,技术更新迭代的速度也在加快.看着Java.Js.Swift在各领 ...

  5. 推荐25款实用的 HTML5 前端框架和开发工具【下篇】

    快速,安全,响应式,互动和美丽,这些优点吸引更多的 Web 开发人员使用 HTML5.HTML5 有许多新的特性功能,允许开发人员和设计师创建应用程序和网站,带给用户桌面应用程序的速度,性能和体验. ...

  6. HTML5 前端框架和开发工具【下篇】

    HTML5 前端框架和开发工具[下篇] 快速,安全,响应式,互动和美丽,这些优点吸引更多的 Web 开发人员使用 HTML5.HTML5 有许多新的特性功能,允许开发人员和设计师创建应用程序和网站,带 ...

  7. Spring框架的第四天(整合ssh框架)

    ## Spring框架的第四天 ## ---------- **课程回顾:Spring框架第三天** 1. AOP注解方式 * 编写切面类(包含通知和切入点) * 开启自动代理 2. JDBC模板技术 ...

  8. DDD实战进阶第一波(四):开发一般业务的大健康行业直销系统(搭建支持DDD的轻量级框架三)

    上一篇文章我们讲了经典DDD架构对比传统三层架构的优势,以及经典DDD架构每一层的职责后,本篇文章将介绍基础结构层中支持DDD的轻量级框架的主要代码. 这里需要说明的是,DDD轻量级框架能够体现DDD ...

  9. DDD实战进阶第一波(三):开发一般业务的大健康行业直销系统(搭建支持DDD的轻量级框架二)

    了解了DDD的好处与基本的核心组件后,我们先不急着进入支持DDD思想的轻量级框架开发,也不急于直销系统需求分析和具体代码实现,我们还少一块, 那就是经典DDD的架构,只有了解了经典DDD的架构,你才能 ...

随机推荐

  1. zookeeper基本配置以及一些坑

    配置 1. 解压安装包:tar zxvf zookeeper-3.4.14.tar.gz 2. 修改zookeeper配置: #Master cd zookeeper-3.4.14 #创建日志文件夹及 ...

  2. IntelliJ IDEA 2019 的安装与破解

    IDEA 全称 IntelliJ IDEA,是java编程语言开发的集成环境.IntelliJ在业界被公认为最好的java开发工具,尤其在智能代码助手.代码自动提示.重构.J2EE支持.各类版本工具( ...

  3. Java高级特性———Java注解

    什么是注解(Annotation)? 注解是放在Java源码的类.方法.字段.参数上的一种标签,在Java SE 5.0版本中开始引入.注解同class和interface一样,也属于一种类型. 如何 ...

  4. idea创建mybatis的逆向工程:generator

    1在工程的目录下配置mybatis-generator插件 <build> <plugins> <!-- mybatis逆向工程 --> <plugin> ...

  5. 8点了解Java服务端单元测试

    一. 前言 单元测试并不只是为了验证你当前所写的代码是否存在问题,更为重要的是它可以很大程度的保障日后因业务变更.修复Bug或重构等引起的代码变更而导致(或新增)的风险. 同时将单元测试提前到编写正式 ...

  6. spring-boot如何生成元数据与javaBean进行关联用作配置文件提示

    spring-boot如何生成元数据与javaBean进行关联用作配置文件提示 首先需要引入一个jar依赖包,以及一个maven plugin如下所示 <dependency> <g ...

  7. source insight4工具栏还原

    source insight4工具栏不小心动了一下,位置全变了,强迫症犯了. 还原步骤. 1.关闭SI4 2.备份 C:\Users\用户名\Documents\Source Insight 4.0\ ...

  8. centos开放指定端口

    1.开启防火墙      systemctl start firewalld 2.开放指定端口       firewall-cmd --zone=public --add-port=1935/tcp ...

  9. Kubernetes-12:Secret介绍及演示

    Secret介绍 Secret存在的意义 Secret解决了密码.token.密钥等敏感数据的配置问题,而不需要把这些敏感数据暴露到镜像或者Pod Spec中,可以以Volume或者环境变量的方式使用 ...

  10. C Primer Plus 学习笔记

    随笔: 1)C语言中%3d%2d什么意思? 格式化规定字符, 以"%"开始, 后跟一个或几个规定字符,用来确定输出内容格式.在"%"和字母之间插进数字表示最大场 ...