Next輕量級框架與主流工具的整合

前言

老大說之後會用 next 來作一下 SSR 的項目,讓咱們有空先學學。又從 0 開始學習新的東西了,想着仍是記錄一下學習歷程,有輸入就要有輸出吧,省得之後給忘記學了些什麼~javascript


Next框架與主流工具的整合

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

首先,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出品的。html

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

  • 基於上面的項目結構整合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 => {
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();
  }
};

三、請求攔截、loading及錯誤處理

  • 封裝fetch請求,使用單例模式對請求增長全局loading等處理。
要點:一、單例模式。二、延遲loading。三、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();

寫在最後

一個完整項目的雛形大體出來了,可是仍是須要在實踐中不斷打磨和優化。node

若有錯誤和問題歡迎各位大佬不吝賜教 :)react

Next輕量級框架與主流工具的整合(二)—— 完善與優化android

相關文章
相關標籤/搜索