老大說之後會用 next 來作一下 SSR 的項目,讓咱們有空先學學。又從 0 開始學習新的東西了,想着仍是記錄一下學習歷程,有輸入就要有輸出吧,省得之後給忘記學了些什麼~javascript
github地址:https://github.com/code-coder/next-mobile-complete-appcss
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" ],
"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.
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" }
-- 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
ant design
是我使用過並且比較有好感的UI框架。既然這是移動端的項目,ant design mobile 成了首選的框架。我也看了其餘的主流UI框架,如今流行的UI框架有Amaze UI、Mint UI、Frozen UI等等,我的仍是比較喜歡ant
出品的。html
剛好templates中有ant design mobile的demo:with-ant-design-mobile。java
with-ant-design-mobile
這個demo。{ "presets": ["next/babel"], "plugins": [ [ "import", { "libraryName": "antd-mobile" } ] ] }
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; } });
(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; } })();
<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 || || -- Layout.js || || -- NavBar.js || ++ tabs || || -- TabHome.js || || -- TabIcon.js || || -- TabTrick.js || || -- Tabs.js
nav | |
---|---|
content | |
tabs |
nav | |
---|---|
content |
// 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。三、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