前言

老大说以后会用 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

最新文章

  1. leetcode 189
  2. 戴文的Linux内核专题:06配置内核(2)
  3. 十八、mysql 内存优化 之 myisam
  4. Nginx 简单的负载均衡配置示例
  5. WordPress插件制作笔记(三)---Stars Comments Article
  6. CSS-负边距原理
  7. jquery.validationEngine
  8. 安卓高级 特效动画ExplosionField和 SmoothTransition
  9. wcf生成客户端代理类步骤及语句
  10. BZOJ1997 平面图判定 平面图性质 2-sat
  11. eclipse添加插件实现php的增删改查
  12. winform跨线程访问控件
  13. 题解——loj6281 数列分块入门5 (分块)
  14. Linux环境下Java中文乱码解决方案
  15. html页面跳转传递参数
  16. kettle Spoon.bat运行闪退
  17. mysql导入source注意点
  18. photoshop,钢笔工具锚点类型
  19. 页面元素定位 XPath 简介
  20. Java精选笔记_集合概述(Collection接口、Collections工具类、Arrays工具类)

热门文章

  1. Kubernetes K8S之资源控制器StatefulSets详解
  2. Windows Server 2012 R2 时间同步
  3. Windows Server 2012 数据库定时备份
  4. android studio生成aar包
  5. python中os.path下模块总结
  6. 这应该是最适合国内用户的K3s HA方案
  7. Inscribed Figures(思维)
  8. web前端常见安全问题
  9. vue 图片路径问题
  10. C014:不用算术分割显示逆序三位数