Next輕量級框架與主流工具的整合
阿新 • • 發佈:2018-12-26
前言
老大說以後會用 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新增引用:
- 構造佈局
- 在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();
寫在最後
一個完整專案的雛形大致出來了,但是還是需要在實踐中不斷打磨和優化。
如有錯誤和問題歡迎各位大佬不吝賜教 :)