Next轻量级框架与主流工具的整合
前言
老大说以后会用 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 UI、Mint UI、Frozen 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>
```
- 构造布局
 
- 在components文件夹新增layout和tabs文件夹
 
++ components
|| ++ layout
|| || -- Layout.js
|| || -- NavBar.js
|| ++ tabs
|| || -- TabHome.js
|| || -- TabIcon.js
|| || -- TabTrick.js
|| || -- Tabs.js
- 应用页面大致结构是(意思一下)
 
- 首页
 
| 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 => {
if (window && window.history.length > 0) {
  !this.setState.canGoBack && this.setState({ canGoBack: true });
} else {
  this.setState.canGoBack && this.setState({ canGoBack: false });
}
};
// NavBar.js
let onLeftClick = () => {
  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(() => {
        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 => {
        if (params[key] instanceof Array) {
          const value = params[key].map(item => '"' + item + '"');
          paramsArray.push(key + '=[' + value.join(',') + ']');
        } else {
          paramsArray.push(key + '=' + params[key]);
        }
      });
      if (url.search(/\?/) === -1) {
        url += '?' + paramsArray.join('&');
      } else {
        url += '&' + paramsArray.join('&');
      }
    }
    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 && !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) => {
        setTimeout(() => reject(new Error('request timeout')), REQUEST_TIEM_OUT);
      })
    ])
      .then(response => {
        !isServer && !noLoading && 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 && response.json();
        }
      })
      .catch(e => {
        if (!isServer && !noLoading) {
          this.hideLoading();
          Toast.info(e.message);
        }
        return { success: false, statusText: e.message };
      });
  }
}
export default ProxyFetch.getInstance();
写在最后
一个完整项目的雏形大致出来了,但是还是需要在实践中不断打磨和优化。
如有错误和问题欢迎各位大佬不吝赐教 :)
来源:https://segmentfault.com/a/1190000016383263
Next轻量级框架与主流工具的整合的更多相关文章
- Java EE互联网轻量级框架整合开发— SSM框架(中文版带书签)、原书代码
		
Java EE互联网轻量级框架整合开发 第1部分 入门和技术基础 第1章 认识SSM框架和Redis 2 1.1 Spring框架 2 1.2 MyBatis简介 6 1.3 Spring MVC简介 ...
 - 互联网轻量级框架SSM-查缺补漏第一天
		
简言:工欲其事必先利其器,作为一个大四的准毕业生,在实习期准备抽空补一下基础.SSM框架作为互联网的主流框架,在会使用的基础上还要了解其原理,我觉得会对未来的职场会有帮助的.我特意的买了一本<J ...
 - 互联网轻量级框架SSM-查缺补漏第八天(MyBatis插件plugin使用及原理)
		
简言:今天进行第八天的记录(只是写了八天).有的时候看的多,有的时候看的少,看的少的时候就攒几天一起写了.而今天这个插件我昨天写了一下午,下班没写完就回去了,今天把尾收了,再加上一个过程图方便下面原理 ...
 - 前端Js框架汇总(工具多看)
		
前端Js框架汇总(工具多看) 一.总结 一句话总结: 二.前端Js框架汇总 概述: 有些日子没有正襟危坐写博客了,互联网飞速发展的时代,技术更新迭代的速度也在加快.看着Java.Js.Swift在各领 ...
 - 推荐25款实用的 HTML5 前端框架和开发工具【下篇】
		
快速,安全,响应式,互动和美丽,这些优点吸引更多的 Web 开发人员使用 HTML5.HTML5 有许多新的特性功能,允许开发人员和设计师创建应用程序和网站,带给用户桌面应用程序的速度,性能和体验. ...
 - HTML5 前端框架和开发工具【下篇】
		
HTML5 前端框架和开发工具[下篇] 快速,安全,响应式,互动和美丽,这些优点吸引更多的 Web 开发人员使用 HTML5.HTML5 有许多新的特性功能,允许开发人员和设计师创建应用程序和网站,带 ...
 - Spring框架的第四天(整合ssh框架)
		
## Spring框架的第四天 ## ---------- **课程回顾:Spring框架第三天** 1. AOP注解方式 * 编写切面类(包含通知和切入点) * 开启自动代理 2. JDBC模板技术 ...
 - DDD实战进阶第一波(四):开发一般业务的大健康行业直销系统(搭建支持DDD的轻量级框架三)
		
上一篇文章我们讲了经典DDD架构对比传统三层架构的优势,以及经典DDD架构每一层的职责后,本篇文章将介绍基础结构层中支持DDD的轻量级框架的主要代码. 这里需要说明的是,DDD轻量级框架能够体现DDD ...
 - DDD实战进阶第一波(三):开发一般业务的大健康行业直销系统(搭建支持DDD的轻量级框架二)
		
了解了DDD的好处与基本的核心组件后,我们先不急着进入支持DDD思想的轻量级框架开发,也不急于直销系统需求分析和具体代码实现,我们还少一块, 那就是经典DDD的架构,只有了解了经典DDD的架构,你才能 ...
 
随机推荐
- Spring 的Controller 是单例or多例
			
Spring 的Controller 是单例or多例 你什么也不肯放弃,又得到了什么? 背景:今天写代码遇到一个Controller 中的线程安全问题,那么Spring 的Controller 是单例 ...
 - 如何分析和监测竞争对手网站的seo数据
			
http://www.wocaoseo.com/thread-36-1-1.html 如何分析和监测况争对手的网站的seo操作方法和seo数据?主要从哪几个方面考虑?如何分析和监测竞争对手网站的seo ...
 - 高效IO解决方案-Mmap「给你想要的快」
			
随着技术的不断进步,计算机的速度越来越快.但是磁盘IO速度往往让欲哭无泪,和内存中的读取速度有着指数级的差距:然而由于互联网的普及,网民数量不断增加,对系统的性能带来了巨大的挑战,系统性能往往是无数技 ...
 - 纯CSS3图片反转
			
一些简单实用的小技巧,CSS3对图片进行翻转,显示另一面的文字,或者图片效果,那么具体怎样去做呢?一起来看看吧. 在CSS3中,可以使用transform-style: preserve-3d进行3d ...
 - 蒲公英 · JELLY技术周刊 Vol.20: Vue3 极致优化——分析 Vue3 Compiler 告诉你为什么这么快
			
蒲公英 · JELLY技术周刊 Vol.20 性能优化是一条无尽的路,我们总是可以找到各种途径去提升体验,不论是响应时间还是按需加载,亦或是根据框架或者组件有针对性的优化都会是不错的方法.如果你在使用 ...
 - 【Android】listview 嵌套gridview报错,代码:”during second layout pass: posting in next frame
			
作者:程序员小冰,CSDN博客:http://blog.csdn.net/qq_21376985, QQ986945193 公众号:程序员小冰 说明:本人曾经在listview嵌套gridview出现 ...
 - Android开发之 。。各种Adapter的用法
			
同样是一个ListView,可以用不同的Adapter让它显示出来,比如说最常用的ArrayAdapter,SimpleAdapter,SimpleCursorAdapter,以及重写BaseAdap ...
 - Sql 注入----学习笔记
			
先了解下CRLF,CRLF常用在分隔符之间,CR是carriage retum(ASCII 13,\r) LF是Line Feed (ASCII 10,\n), \r\n这两个字符类似于回车是用于换行 ...
 - ugui 自定义字体
			
Unity/UI —— 使用字符图片自定义字体(Custom Font) ---[佳] https://blog.csdn.net/qq_28849871/article/details/777190 ...
 - 面试【JAVA基础】Web与网络
			
1.转发与重定向的区别 转发是服务器请求资源,服务器直接访问目标地址url,把响应内容返回给浏览器. 重定向根据服务器返回的状态码重新请求地址. 转发是服务器行为,重定向是客户端行为. 转发显示的ur ...