能同步傳送微信公眾號訊息的部落格系統
用React、Redux和wechat-es搭建一個能同步傳送微信公眾號訊息的部落格系統,而且,要支援各種文章樣式。
- git、npm、webpack和babel手拉手
- 整合一個爽到飛起的編輯器
git、npm、webpack和babel手拉手
標題中這四個應該是開發Node.js應用的主流工具了吧?雖然babel是個過渡性工具,但估計這個過渡期會比較長。
我想有個庫
經歷過壓縮檔案、CVS和Subversion的人才能真正懂得git的好。
有些人生來就註定能領導幾百萬人,有些人生來就註定能寫出翻天覆地的軟體。但只有一個人兩樣都能做到:託瓦茲。
而且不止一次做到!(PS:強烈抗議輸入法在我想輸入托瓦茲時提示“脫襪子”!!!)
不知道你們怎樣,總之我每次用GitHub時都會在內心深處向這個神一般的男人致敬:
順便吹捧下自己,鄙人曾有幸跟該書的著名譯者陳少芸先生合譯過一本書;更榮幸的是,還有合影:
哦,右一是我。好吧,也不能算是合影,畢竟我是真身出鏡。
還是聊專案吧。
首先,要在GitHub上建立一個程式碼庫,名字就叫webchat-blog。GitHub看我什麼也沒說,很體貼地甩了幾條命令出來。我乖乖複製下來,準備貼上到終端中執行:
echo "# wechat-blog" >> README.md
git init
git add README.md
git commit -m "first commit"
git remote add origin git@github.com:wuhaixing/wechat-blog.git
git push -u origin master
當然要先建立專案目錄:
mkdir wechat-blog && cd wechat-blog
然後有選擇地粘帖。由於還有很多工作要做,先不要commit,更不要push,把這兩條命令放到一邊備用。
接下來初始化npm專案:
npm init -y
然後開啟package.json檔案,加上"private": true
,免得一不小心把它提交到npmjs上。npmjs是公共場合,我為了佔名字把還沒完成的wechat-es publish上去了,估計會被很多人罵。
配置webpack
webpack是個挺好用的構建工具,自帶web伺服器,還支援模組熱切換,配置起來也不難。當然,別的構建工具也都是這麼說的,自己喜歡哪個用哪個吧。反正這個專案就用它了,先安裝:
npm i -D webpack webpack-dev-server
如果你想在終端中直接執行webpack,請加上全域性安裝的選項-g,不過我習慣在npm裡呼叫,所以就省了。
然後建立webpack.config.js檔案,並新增基本配置:
module.exports = {
context: __dirname + "/app",
entry: "./app.js",
output: {
filename: "app.js",
path: __dirname + "/dist",
},
}
這個配置是告訴webpack,我們的應用放在app目錄下,入口檔案是app.js;並且請webpack把它的編譯結果放到dist目錄下,檔名仍然用app.js。
這個基本配置只是把檔案複製到dist目錄下,而webpack真正強大之處在於它可以在複製之前用各種loader對檔案進行處理。為了處理ES 2015和React中的JSX,需要安裝babel-loader,以及它的小夥伴們:
npm i -D babel-core babel-loader babel-preset-es2015 babel-preset-react
看babel-core這名字就不用問了,core,不解釋!兩個preset的名字也很直白,分別是處理es2015和react的。裝好之後在webpack.config.js中加個配置項:
module: {
loaders: [
{
test: /\.js$/,
exclude: /node_modules/,
loaders: ["babel-loader"],
}
],
},
loader可以有很多個,不過要放在module
裡,一個個的在loaders
裡排好。其中的test
比較敏感,不過它不是你們想的那種test,應該叫match,檔名符合後面這個正規表示式的都要處理。exclude
就是排除。loaders
裡的就是要對符合test
條件的檔案使用的loaders,首先是babel-loader。綜上所述,這個配置是讓webpack對所有.js檔案(除了node_modules中的)應用babel-loader。
為了告訴babel-loader將es 6和jsx語句轉換成es 5,還要在package.json中配置babel的preset:
"babel": {
"presets": [
"es2015",
"react"
]
}
一個小確能的React應用
做好了基本配置,我們就可以開始寫React元件了。當然,還是要從安裝開始:
npm i -S react react-dom
先寫一個元件擺擺樣子:
import React from 'react'
class Greeting extends React.Component {
render() {
return <div className="greeting">
Hello, {this.props.name}!
</div>
}
}
export default Greeting
然後在app/app.js中把這個元件渲染到頁面中:
import React from 'react'
import ReactDOM from 'react-dom'
import Greeting from "./greeting"
ReactDOM.render(
<Greeting name="World"/>,
document.getElementById("app")
)
接下來新增app/index.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Wechat Blog</title>
</head>
<body>
<div id="app"></div>
</body>
<script src="app.js"></script>
</html>
這是個html檔案,之前webpack的loader只處理js檔案,所以還要為它再新增一個loader。
再新增一個loader
這個loader叫file-loader,安裝:
npm i -D file-loader
然後在loaders中新增配置處理html檔案:
{
test: /\.html$/,
loader: "file?name=[name].[ext]",
},
你可能注意到了,前面我們用的是loaders:[]
,這裡直接loader
。因為前面那個還要再新增一個loader,用來實現react的熱載入。
再新增一個loader
這個loader是配合webpack-dev-server用的。有了它,我們每次修改了react元件後,不用去終端裡執行編譯命令,不用到瀏覽器上點重新整理按鈕,修改就自動體現到頁面上了。
npm i -D react-hot-loader
找到第一個loader,把它加到陣列中就可以了。就像這樣:
{
test: /\.js$/,
exclude: /node_modules/,
loaders: ["react-hot", "babel-loader"],
},
好了,所有的loader都到場了,我們可以開始了。在package.json中加上:
"scripts": {
"start": "webpack-dev-server --hot --inline"
},
--hot --inline
這兩個選項就是告訴webpack-dev-server,我們要熱!加!載!
在終端中執行npm start
,在瀏覽器中開啟http://localhost:8080/,如果能看到Hello,World!那說明我們的六公里慢跑成功地邁出了第一步!
想看程式碼請直接:
git clone https://github.com/wuhaixing/wechat-blog
參考文件
Setting Up Webpack for React and Hot Module Replacement
整合一個爽到飛起的編輯器
要做blog系統,即便是非常簡單的,選一個好用的編輯器也是最起碼的操守。所以我搜了一整天,終於找到了Alloy Editor。它在自己的demo頁面上是這樣介紹自己的:
在老牌編輯器CKEditor的基礎上搭建起來的所見即所得編輯器,在頁面上點一下就可以直接編輯。編輯內容爽到飛起。。。
參照它在文件Creating a React component中提到的alloyeditor-react-component,我把Alloy Editor整合到了我們的專案中。不過實現和這個例子有些不同:
- 沒用gulp來增加構建系統的複雜性。而是在webpack新增了
copy-webpack-plugin
,以便將alloyeditor目錄複製到dist中; - 去掉了沒什麼用處的server.js
- 將editor和client改成了es 6語法
給webpack新增一個可以複製目錄的外掛
copy-webpack-plugin可以複製單獨的檔案或目錄。
安裝:
npm i -D copy-webpack-plugin
在webpack.config.js中引入:
var CopyWebpackPlugin = require('copy-webpack-plugin');
然後在plugins中建立一個新物件:
plugins: [
new CopyWebpackPlugin([
{ from: '../node_modules/alloyeditor/dist' }
])
]
其實它有很多可配置的引數,不過只有from
是必須的,比較常用的是{ from: 'source', to: 'dest' }
,我們只需要用from
指定alloyeditor所在的源目錄,目標目錄就是output中指定的dist。
將alloyeditor封裝到React元件中
我沒看懂alloyeditor-react-component為什麼要建立一個server.js。我的實踐也證明確實不需要,只需要建立一個editor.js:
import React from 'react'
import AlloyEditor from 'alloyeditor'
export default class Editor extends React.Component {
componentDidMount() {
this._editor = AlloyEditor.editable(
this.props.container,
this.props.alloyEditorConfig)
}
componentWillUnmount() {
this._editor.destroy()
}
render() {
return <div id={this.props.container}>
{this.props.content}
</div>
}
}
在componentDidMount
中初始化AlloyEditor
,在componentWillUnmount
中destroy
它。然後render
方法中返回交給它的內容就可以了。
這個元件的用法跟其它元件都一樣,在app.js中:
const content = <div>
<h1>請點選頁面編輯</h1>
<p>編輯本頁內容</p>
</div>
ReactDOM.render(
<Editor container="editable" content={content}/>,
document.getElementById("app")
)
告訴它可編輯區塊的id
和要編輯的內容就可以了。
改動比較大的是index.html,要引入alloyeditor的樣式,並定義ALLOYEDITOR_BASEPATH
和CKEDITOR_BASEPATH
兩個全域性變數:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Webpack + React</title>
<link href="alloy-editor/assets/alloy-editor-ocean-min.css" rel="stylesheet">
<style>
#app {
left: 100px;
position: relative;
top: 100px;
}
</style>
<script>
window.ALLOYEDITOR_BASEPATH = 'alloy-editor/';
window.CKEDITOR_BASEPATH = 'alloy-editor/';
</script>
</head>
<body>
<div id="app"></div>
</body>
<script src="app.js"></script>
</html>
這裡指定了alloy-editor/
,是因為在webpack.config.js中,將/node_modules/alloyeditor/dist
目錄中的內容複製到了/dist
目錄下,如果複製的目標目錄不同,這裡也要做相應的修改。
在終端中執行npm start
,開啟http://localhost:8080/,就能看到一個點選就能編輯的頁面了。
3R闖前端之react-router
第一個R是React,我們之前已經用它建立了一個元件。但React只是個建立前端元件的庫,不是框架,所以光靠它不足以撐起整個前端。另外兩個是react-router和redux,它們都是因React而起。雖然redux適用範圍很廣,但它們三個是最常見的組合。結合wechat-blog,我們來看一下如何用它們完成前端開發中的任務。
先說react-router。
react-router致力於解決單頁應用中的兩個問題:一個是頁面佈局,另一個是路由。
沒有 VS 有
其實react-router本身只是一個React元件庫,它所做的工作都可以通過自己編寫元件的方式完成。下面這個例子來自react-router的Introduction:
import React from 'react'
import { render } from 'react-dom'
const About = React.createClass({/*...*/})
const Inbox = React.createClass({/*...*/})
const Home = React.createClass({/*...*/})
const App = React.createClass({
getInitialState() {
return {
route: window.location.hash.substr(1)
}
},
componentDidMount() {
window.addEventListener('hashchange', () => {
this.setState({
route: window.location.hash.substr(1)
})
})
},
render() {
let Child
switch (this.state.route) {
case '/about': Child = About; break;
case '/inbox': Child = Inbox; break;
default: Child = Home;
}
return (
<div>
<h1>App</h1>
<ul>
<li><a href="#/about">About</a></li>
<li><a href="#/inbox">Inbox</a></li>
</ul>
<Child/>
</div>
)
}
})
render(<App />, document.body)
這個元件的componentDidMount
方法中定義了window
的hashchange
事件監聽器,它會根據hash url的變化改變state中的route
值;元件的render
方法又會根據route
的值來給Child
賦值,從而改變頁面的渲染結果。
react-router的Introduction緊接著又給出了使用react-router完成這一任務的例子:
import React from 'react'
import { render } from 'react-dom'
// 接下來出場的是react-router的主要成員...
import { Router, Route, IndexRoute, Link, hashHistory } from 'react-router'
// App一下子變得簡單了
// <Link>取代了<a>,#/也不見了...
const App = React.createClass({
render() {
return (
<div>
<h1>App</h1>
{/* change the <a>s to <Link>s */}
<ul>
<li><Link to="/about">About</Link></li>
<li><Link to="/inbox">Inbox</Link></li>
</ul>
{/*
`<Child>`變成了`this.props.children`
router會幫我們找出應該讓哪個child登場
*/}
{this.props.children}
</div>
)
}
})
// 最後,渲染的是帶了一堆小<Route>的<Router>。
// 一切都由它們負責搞定
render((
<Router history={hashHistory}>
<Route path="/" component={App}>
<IndexRoute component={Home} />
<Route path="about" component={About} />
<Route path="inbox" component={Inbox} />
</Route>
</Router>
), document.body)
這次render
方法渲染的不是App
了,換成了react-router提供的元件<Router>
,<Router>
之下又有一組子元件<Route>
,每個元件<Route>
都有path
和component
兩個屬性;<Route>
也可以有自己的子元件<Route>
。哦,還有<IndexRoute>
,它的component
實際上對應上層<Route>
元件指定的path
。而上層元件的component
就起到了決定頁面佈局的作用。
在wechat-blog中,路由的定義放在app/router.js檔案中。結構跟上面例子中的一樣,只是Router
的history
換成了browserHistory
,而作為頁面佈局的元件名稱上直接定義為MainLayout
。
<Router history={browserHistory}>
<Route component={MainLayout}>
<Route path="/" component={Home} />
<Route path="posts">
<Route component={SearchLayoutContainer}>
<IndexRoute component={PostListContainer} />
</Route>
<Route path="new" component={PostFormContainer} />
<Route path=":postId" component={PostContainer} />
</Route>
<Route path="users">
<Route component={SearchLayoutContainer}>
<IndexRoute component={UserListContainer} />
</Route>
<Route path=":userId" component={UserProfileContainer} />
</Route>
<Route path="widgets">
<Route component={SearchLayoutContainer}>
<IndexRoute component={WidgetListContainer} />
</Route>
</Route>
</Route>
</Router>
路徑中的變數
此外還有一點需要注意的是引數的傳遞,也就是路由中的動態部分。在Route
的定義中,動態引數是用冒號作字首表示的:
,比如<Route path=":postId" component={PostContainer} />
。
react-router會把url中的動態部分放到元件的props
中,比如在app/components/containers/post-container.js中:
componentDidMount: function() {
let postId = this.props.params.postId
if(postId) {
postApi.getPost(postId)
}
}
動態url的生成也很簡單,在app/components/views/post-list.js中有:
<Link to={'/posts/' + post.id}>{post.title}</Link>
跳轉
有時候我們需要跳轉到不同的url來顯示相應的介面,比如在儲存了一個post之後,希望能夠顯示post列表。這也很容易實現,在app/components/containers/post-form-container.js中有個例子,關鍵是下面這行程式碼:
browserHistory.push('/posts')
關於react-router,基本上就是這些了。
3R闖前端之redux
第三個R是redux。react簡化了資料顯示元件的建立和管理問題,react-router解決了單頁應用的頁面模板和路由問題。雖然redux官方只說它解決的是狀態管理問題,但實際上它還極大降低了react元件間的耦合性。
先看圖:
redux提供了一個儲存狀態的store
,外界可以通過呼叫store.dispatch
傳遞一個代表狀態變化的action
給它。在上圖右側,react元件的狀態有變化時就dispatch
一個action
,然後所有跟store繫結的元件的狀態都會相應地發生變化。元件之間不需要相互傳遞資料,極大降低了元件之間的耦合性。
具體實現時,大體上是下圖這樣的關係及流程:
- 當React元件需要獲取資料或提交資料時,會呼叫相應的業務處理邏輯函式,即上圖中的API函式。
- 這些API函式一般會向伺服器傳送請求,然後在得到響應結果後呼叫
store
的dispatch
函式;store.dispatch
的引數就是ActionCreator的返回結果,Redux稱之為Action。 - redux會將這些Action傳給reducer,reducer會根據Action的型別進行處理,然後返回新的狀態,由redux統一更新到它的狀態庫中。
- redux的狀態更新能夠傳遞到React元件中,主要歸功於redux-react。我們要在React元件中呼叫connect函式,並將其返回結果作為預設輸出。
我們結合wechat-blog來看一下具體的實現,首先從React元件開始。
Provider與connect
Provider
是redux-react提供的一個React元件。之前在介紹react-router時,Router
取代App
成為應用的頂層元件。現在Router
要讓位給Provider
了,在應用的入口檔案app/app.js中:
ReactDOM.render(
<Provider store={store}>{router}</Provider>,
document.getElementById('root')
);
Provider
有個store
屬性,它的所有子元件,實際上也就是所有React元件,都可以通過它訪問到這個store
,從而得到redux狀態庫中的所有狀態。這個任務是由connect
完成的,比如在app/components/containers/post-container.js中,可以看到:
const mapStateToProps = function(store) {
return {
post: store.postState.post
};
};
export default connect(mapStateToProps)(PostContainer);
函式mapStateToProps
將store.postState.post
賦值給post
,這個函式成了connect
的引數,而PostContainer
是connect
返回結果的引數。
通過redux-react的努力,React元件就這樣跟redux的狀態庫store連線起來了,接下來我們去看看這個神祕的store
究竟長什麼樣。
store,reducers、Action與ActionCreator
store
非常簡單,在app/store.js中只有四行程式碼:
import { createStore } from 'redux';
import reducers from './reducers';
const store = createStore(reducers);
export default store;
只是用reducers
作為引數,通過redux提供的createStore
函式來建立它。
在app/reducers/index.js中,最重要的就是redux提供的combineReducers
,它的作用很簡單,只是把多個分散的reducer合併到一起。想象一下,如果所有reducer的程式碼都只能放在一個檔案裡......
reducer的程式碼也很直白簡單,以app/reducers/post-reducer.js為例,只是一個主體為switch
語句的函式而已:
switch(action.type) {
case types.GET_POSTS_SUCCESS:
return Object.assign({}, state, { posts: action.posts });
....
}
不過有一點非常重要:絕對不要修改狀態!,redux的文件說了:“修改狀態的唯一途徑是發出action(一個描述將要發生什麼的物件)”。在上面的例子中用了Object.assign({}, state, { posts: action.posts });
,這會合並state
和{ posts: action.posts }
建立一個新物件。Object.assign是ES 6的新特性,IE目前還不支援。也可以用一些庫來達到同樣的目的,比如 Facebook的Immutable.js,此外還有seamless-immutable、Mori等。
另外,reducer必須是純函式。所謂的純函式,就是要符合下述條件:
- 不會呼叫外部資源,比如網路或資料庫;
- 輸出僅由輸入決定,即只要引數的值相同,則輸出結果一定相同;
- 輸入的引數應該被當做不可變值,絕不能修改;
接下來要介紹的action和action creator就更簡單了。前面說過了,action就是一個普通的物件,它的特別之處在於必須要有個屬性指明其型別。為了安全起見,最好把所有action的型別都集中放在一個檔案中。
對於某一項操作,一般會定義兩種action,分別是XXX_SUCCESS
和XXX_FAILED
。action creator相當於將元件要傳遞的資料map成action的簡單函式,比如在app/actions/post-actions.js中:
export function getPostsSuccess(posts) {
return {
type: types.GET_POSTS_SUCCESS,
posts
};
}
這些都準備好之後,整幅拼圖就剩下最後一塊了,API。
API
雖然叫API,但實際上仍然是客戶端的操作,只是把業務邏輯請求從React元件裡剝離了出來而已。在這一層,要解決的是兩個問題,一是跟服務端的互動;二是傳送action。我們看一下app/api/post-api.js:
export function getPosts() {
return axios.get('http://localhost:3001/posts')
.then(response => {
store.dispatch(getPostsSuccess(response.data));
return response;
});
}
axios可以向伺服器端傳送請求,其返回結果是Promise,如果不知道Promise是個什麼鬼,請參考有Promise,不會搞大肚子。然後在then
中用store.dispatch
發出事件,redux狀態庫中的資料就會相應地變化,而react元件的props是跟它綁在一起的,自然也會發生變化。
前端的路線就是這樣。
參考文獻
相關文章
- 微信公眾號如何實現模板訊息傳送的功能
- 微信公眾號系統
- 部落格&公眾號管理
- 微信公眾號開發之客服訊息
- Java微信公眾號推送模版訊息的方法示例Java
- 微信公眾號傳送模板訊息,出現亂碼問題---字元中文編碼問題字元
- 【微信部落】tp5+ionic開發微信公眾號商城系統
- 微信公眾號客服系統-接收對話方塊文字圖片影片訊息
- 如何打通 SAP Cloud for Customer 系統和微信公眾號的雙向訊息通訊功能Cloud
- 微信公眾號開發(二)識別訊息型別型別
- 微信公眾平臺開發(十二) 傳送客服訊息
- java實現 微信公眾號推送訊息 ,cv 就可執行!!!Java
- 公眾號傳送模板資訊java實現(主動傳送)Java
- 微信公眾號接入線上客服系統的方式
- 微信公眾號上如何上傳excel表格?Excel
- 微信公眾號影片直播系統開發介紹
- Rocket MQ 的三種訊息傳送(同步、非同步、單向)和訊息訂閱MQ非同步
- 微信公眾號支付IOS系統能夠喚起,安卓系統不能喚起的問題解決iOS安卓
- Go 實戰丨微信公眾號接入及使用者訊息處理Go
- 【C#版本】微信公眾號模板訊息對接(二)(圖文詳解)C#
- 如何下載微信公眾號的音訊檔案音訊
- 使用 laravel-wechat-notification 傳送微信模板訊息、企業微信應用訊息Laravel
- 微信小程式 傳送模板訊息的功能實現微信小程式
- 微信公眾號智慧回答
- 微信公眾號開發
- 微信公眾號--入門
- thinkphp5.0.11開發微信公眾號通用系統PHP
- 微信公眾號投票活動製作教程 微信公眾號投票怎麼弄?
- 在python中使用itchat傳送微信訊息Python
- 如何快速實現公眾號群發模板訊息?
- 小程式中使用公眾號模板訊息思路整理
- python + flask 開發的微信公眾號和文章管理系統PythonFlask
- ASP.NET微信公眾號用於給指定OpenId使用者傳送紅包ASP.NET
- 微信公眾號-入門的坑
- 微信公眾號選單的配置
- 教你如何下載微信公眾號的音訊檔案音訊
- 如何下載微信公眾號中的音訊、視訊檔案?音訊
- 微信公眾平臺開發(十) 訊息回覆總結