这是一篇给初学者的教程, 在这篇教程中我们将通过构建一个 Hacker News 的前端页面来学习 React 与 Webpack. 它不会覆盖所有的技术细节, 因此它不会使一个初学者变成大师, 但希望能给初学者一个大致印象.

准备工作

  • 安装 webpack

    在此之前你应该已经安装了 node.js.

    npm install webpack -g

    参数-g表示我们将全局(global)安装 webpack, 这样你就能使用 webpack 命令了.

    webpack 也有一个 web 服务器 webpack-dev-server, 我们也安装上

    npm install webpack-dev-server -g
  • webpack 配置文件

    webpack 使用一个名为 webpack.config.js 的配置文件, 现在在你的项目根目录下创建该文件. 我们假设我们的工程有一个入口文件 app.js, 该文件位于 app/ 目录下, 并且希望 webpack 将它打包输出为 build/ 目录下的 bundle.js 文件.webpack.config.js 配置如下:

    var path = require('path');
    
    module.exports = {
    entry: path.resolve(__dirname, 'app/app.js'),
    output: {
    path: path.resolve(__dirname, 'build'),
    filename: 'bundle.js'
    }
    }

    现在让我们测试一下, 创建 app/app.js 文件, 填入一下内容:

    document.write('It works');

    创建 build/index.html, 填入以下内容:

    <!DOCTYPE html>
    <head>
    <meta charset="UTF-8">
    <title>Hacker News Front Page</title>
    </head>
    <body>
    <script src="./bundle.js"></script>
    </body>
    </html>

      

    其中 script 引入了 bundle.js, 这是 webpack 打包后的输出文件.

    运行 webpack 打包, 运行 webpack-dev-server 启动服务器. 访问 http://localhost:8080/build/index.html, 如果一切顺利, 你会看到打印出了 It works.

  • 配置 package.json

    在项目根目录下运行 npm init -y 会按照默认设置生成 package.json, 修改 scripts 的键值如下:

    "scripts": {
    "start": "webpack-dev-server",
    "build": "webpack"
    }

    现在执行 npm run build 相当于 webpacknpm run start 相当于 webpack-dev-server. 当项目变得相当复杂时, 你可以使用这种技巧隐藏其中的细节.

  • 安装依赖

    安装 React:

    npm install react react-dom --save

    为了简化 AJAX 请求代码, 这里引入 jQuery:

    npm install jquery --save

    安装 Babel 的 loader 以支持 ES6 语法:

    npm install babel-core babel-loader babel-preset-es2015 babel-preset-react --save-dev

    然后配置 webpack.config.js 来使用安装的 loader.

    // webpack.config.js
    
    var path = require('path');
    
    module.exports = {
    entry: path.resolve(__dirname, 'app/app.js'),
    output: {
    path: path.resolve(__dirname, 'build'),
    filename: 'bundle.js'
    },
    module: {
    loaders: [
    {
    test: /\.jsx?$/,
    exclude: /node_modules/,
    loader: 'babel',
    query: {
    presets: ['es2015','react']
    }
    },
    ]
    }
    };

    接下来测试一下开发环境是否搭建完成.

    打开 app.js, 修改内容为:

    // app.js
    
    import $ from 'jquery';
    import React from 'react';
    import { render } from 'react-dom'; class HelloWorld extends React.Component {
    render() {
    return (
    <div>Hello World</div>
    );
    }
    } render(<HelloWorld />, $('#content')[0]);

    这里组件 HelloWorld 被装载到 id 为 content 的 DOM 元素, 所以相应的需要修改 index.html .

    ...
    <body>
    <div id="content"></div>
    <script src="./bundle.js"></script>
    </body>
    ...

    再次打包运行, 访问 http://localhost:8080/build/index.html 如果可以看到打印出了 Hello World 那么开发环境就算搭建完成了.

在开始写第一个组件之前 ...

在开始写第一个组件之前, 让我们分析一下到底我们需要哪些组件.

上图是的最终效果的部分截图, 我们将其分为几个组件, 用不同颜色的线框标出

我们可以看出其中包含以下几个组件.

  • NewsList (蓝色): 所有组件的容器.
  • NewsHeader (绿色): Logo, 标题, 导航栏等.
  • NewsItem (红色): 对应每条资讯.

把这些组成为一棵组件树.

  • NewsList

    • NewsHeader
    • NewsItem * n

编写大致的模板

上一节我们知道了整个页面的组件结构, 在这一节我们根据这些结果编写出一个大致的模板. 先在 app 目录下为每个组件建立文件, NewsList.jsNewsHeader.js 和 NewsItem.js.

编辑 NewsHeader.js

// NewsHeader.js

import React from 'react';

export default class NewsHeader extends React.Component {
render() {
return (
<div className="newsHeader">
I am NewsHeader.
</div>
);
}
}

NewsHeader 组件就先这样, 具体的实现现在先不去考虑, 记得我们现在只是编写一个大致的结构.

同样的, 编辑 NewsItem.js

// NewsItem.js

import React from 'react';

export default class NewsItem extends React.Component {
render() {
return (
<div className="newsItem">
I am NewsItem.
</div>
);
}
}

接着是 NewsList.js, 因为 NewsList 是前两个组件的容器, 所以我们需要引入它们

// NewsList.js

import React from 'react';
import NewsHeader from './NewsHeader.js';
import NewsItem from './NewsItem.js'; export default class NewsList extends React.Component {
render() {
return (
<div className="newsList">
<NewsHeader />
<NewsItem />
</div>
);
}
}

最后修改入口文件 app.js

// app.js

import React from 'react'
import { render } from 'react-dom';
import $ from 'jquery';
import NewsList from './NewsList.js'; render(<NewsList />, $('#content')[0]);

这时访问 http://localhost:8080/build/index.html 可以看到下图的效果

接下来让我们逐步完善各个组件.

NewsHeader

整个 NewsHeader 包含了三个部分, 左边的Logo和标题, 中间的导航栏和右边的登录入口.

让我们先从左边的 Logo 和标题开始.

  1. Logo 和标题

    先下载 Logo

    在 NewsHeader 组件里新增一个方法 getLogo, 就像下面这样.

    // NewsHeader.js
    
    // ...
    
    export default class NewsHeader extends React.Component {
    //...
    getLogo() {
    /* Do Something */
    }
    //...
    }

    这个方法返回一个包含 Logo 的子组件.

    getLogo() {
    return (
    <div className="newsHeader-logo">
    <a href="https://news.ycombinator.com/"><img src="PATH_TO_IMAGE" /></a>
    </div>
    );
    }

    这里遇到一个问题, img 的 src 填什么?

    在前面几节的开发中, 还记得你是怎么引入其他的 js 文件的吗? import. 实际上这是 ES6 的模块系统, 这里的 js 文件作为模块被其他模块引入. 但除了 js 文件, 在开发时我们还会涉及其他的资源文件, 如图像, 字体, 样式等, 它们也需要被模块化. 在这里, 如果 Logo 图片也能被模块化然后引入该多好. 我们需要再次配置 Webpack.

    安装对应的 loader:

    npm install url-loader file-loader --save-dev

    配置 webpack.config.js

    //...
    
     loaders: [
    {
    test: /\.jsx?$/,
    exclude: /node_modules/,
    loader: 'babel',
    query: {
    presets: ['es2015','react']
    }
    },
    {
    test: /\.(png|jpg|gif)$/,
    loader: 'url-loader?limit=8192' // 这里的 limit=8192 表示用 base64 编码 <= 8K 的图像
    }
    ] //...

    然后回到 NewsHeader.js

    这时候你就可以使用 import 引入图片了.

    import imageLogo from './y18.gif';

    然后像这样使用.

    <img src={imageLogo} />

    注意这里用{}包起来, 这样其中的内容会作为表达式.

    getLogo 方法完成了, 再写一个 getTitle 方法.

    getTitle() {
    return (
    <div className="newsHeader-title">
    <a className="newsHeader-textLink" href="https://news.ycombinator.com/">Hacker News</a>
    </div>
    );
    }

    修改 render 方法, 引用我们刚刚写好的那两个方法.

    render() {
    return (
    <div className="newsHeader">
    {this.getLogo()}
    {this.getTitle()}
    </div>
    );
    }

    还是一样别忘了 {}.

    最后我们需要添加样式, 还是回到刚刚的问题, 怎么引入样式?

    我们也需要将样式模块化.

    安装相应的 loader:

    npm install css-loader style-loader --save-dev

    css-loader 处理 css 文件中的 url() 表达式.

    style-loader 将 css 代码插入页面中的 style 标签中.

    在 webpack.config.js 中配置新的 loader.

    {
    test: /\.css$/,
    loader: 'style!css'
    }

    新建一个 css 文件 NewsHeader.css

    .newsHeader {
    align-items: center;
    background: #ff6600;
    color: black;
    display: flex;
    font-size: 10pt;
    padding: 2px;
    } .newsHeader-logo {
    border: 1px solid white;
    flex-basis: 18px;
    height: 18px;
    } .newsHeader-textLink {
    color: black;
    text-decoration: none;
    } .newsHeader-title {
    font-weight: bold;
    margin-left: 4px;
    }

    然后在 NewsHeader.js 中引入它

    import './NewsHeader.css';

    再建立一个全局的 css 文件 app.css

    body {
    font-family: Verdana, sans-serif;
    }

    然后在 app.js 中引入

    import './app.css'

    打包运行看看吧.

  2. 导航栏 接下来是导航栏, 也就是中间那部分.

    回到 NewsHeader.js, 增加一个 getNav 方法.

    getNav() {
    var navLinks = [
    {
    name: 'new',
    url: 'newest'
    },
    {
    name: 'comments',
    url: 'newcomments'
    },
    {
    name: 'show',
    url: 'show'
    },
    {
    name: 'ask',
    url: 'ask'
    },
    {
    name: 'jobs',
    url: 'jobs'
    },
    {
    name: 'submit',
    url: 'submit'
    }
    ]; return (
    <div className="newsHeader-nav">
    {
    navLinks.map(function(navLink) {
    return (
    <a key={navLink.url} className="newsHeader-navLink newsHeader-textLink" href={"https://news.ycombinator.com/" + navLink.url} >
    {navLink.name}
    </a>
    );
    })
    }
    </div>
    );
    }

    同样, 记得在 render 方法中引用

    render() {
    return (
    <div className="newsHeader">
    {this.getLogo()}
    {this.getTitle()}
    {this.getNav()}
    </div>
    );
    }

    添加样式.

    .newsHeader-nav {
    flex-grow:;
    margin-left: 10px;
    } .newsHeader-navLink:not(:first-child)::before {
    content: ' | ';
    }
  3. 登录入口

    增加一个 getLogin 方法.
    
    getLogin() {
    return (
    <div className="newsHeader-login">
    <a className="newsHeader-textLink" href="https://news.ycombinator.com/login?goto=news">login</a>
    </div>
    );
    }
    在 render 中引用 render() {
    return (
    <div className="newsHeader">
    {this.getLogo()}
    {this.getTitle()}
    {this.getNav()}
    {this.getLogin()}
    </div>
    );
    }
    更新样式 .newsHeader-login {
    margin-right: 5px;
    }

至此整个 NewsHeader 就完成了, 你应该能看到如本节初所展示的效果.

NewsItem

如图, 每条资讯对应着这样一个 NewsItem, 本节我们将编写 NewsItem 组件.

可以看到, 一个 NewsItem 包含了资讯的标题, 来源地址, 什么时候发布的以及评论数等等. 它依赖于传入的数据, 那么怎么传入数据呢?

对于父子组件间的通信, 可以使用属性传递. 子组件可以使用 this.props 访问到父组件传入的属性数据.

回到 NewsList 组件, 它作为 NewsItem 的父组件可以使用如下方式传入数据.

<NewsItem item={data} />

NewsItem 中可以使用 this.props.item 访问 item 属性.

像这样我们只需要将资讯数据作为属性传入, 在 NewsItem 中就能获取到了. 让我们开始做吧!

  1. NewsItem 标题

    先来简单点的, 第一步我们只获取并显示标题.

    修改 NewsList.js
    
    render() {
    var testData = {
    "by" : "bane",
    "descendants" : 49,
    "id" : 11600137,
    "kids" : [ 11600476, 11600473, 11600501, 11600463, 11600452, 11600528, 11600421, 11600577, 11600483 ],
    "score" : 56,
    "time" : 1461985332,
    "title" : "Yahoo's Marissa Mayer could get $55M in severance pay",
    "type" : "story",
    "url" : "http://www.latimes.com/business/technology/la-fi-0429-tn-marissa-mayer-severance-20160429-story.html"
    }; return (
    <div className="newsList">
    <NewsHeader />
    <NewsItem item={testData} rank={1} />
    </div>
    );
    }
    这里我们声明一个 testData 作为测试数据传入 NewsItem. 修改 NewsItem.js render: function () {
    return (
    <div className="newsItem">
    <a className="newsItem-titleLink" href={this.props.item.url}>{this.props.item.title}</a>
    </div>
    );
    }
    在这里使用 this.props.item 访问 item 属性. 建立 NewsItem.css .newsItem {
    color: #828282;
    margin-top: 5px;
    align-items: baseline;
    display: flex;
    } .newsItem-titleLink {
    color: black;
    font-size: 10pt;
    text-decoration: none;
    }
    在 NewsItem.js 中引入 import './NewsItem.css';
    运行看看效果, 显示的标题应该和传入的测试数据中的一样. NewsItem 来源地址 我们现在添加来源地址到标题的末尾. 先在 NewsItem.js 中引入 url 模块 import URL from 'url';
    然后增加一个 getDomain 方法. getDomain() {
    return URL.parse(this.props.item.url).hostname;
    }
    然后再增加一个 getTitle 方法, 这个方法会返回一个包含了标题(我们上一节做的事)和地址的组件. getTitle() {
    return (
    <div className="newsItem-title">
    <a className="newsItem-titleLink" href={this.props.item.url}>{this.props.item.title}</a>
    <span className="newsItem-domain"><a href={'https://news.ycombinator.com/from?site=' + this.getDomain()}>({this.getDomain()})</a></span>
    </div>
    );
    }
    修改 render render() {
    return (
    <div className="newsItem">
    <div className="newsItem-itemText">
    {this.getTitle()}
    </div>
    </div>
    );
    }
    增加样式 .newsItem-itemText {
    flex-grow: 1;
    } .newsItem-domain {
    font-size: 8pt;
    margin-left: 5px;
    } .newsItem-domain > a {
    color: #828282;
    text-decoration: none;
    }
    好了, 看起来不错, 但是有个问题, 这个项目最终需要从 Hacker News 的 API 取得资讯数据, 而其中有些是没有 url 属性的, 看看我们的 getTitle() 方法, 我们似乎忽略了这个特例, 让我们做些修改. getTitle() {
    return (
    <div className="newsItem-title">
    <a className="newsItem-titleLink" href={this.props.item.url ? this.props.item.url : 'https://news.ycombinator.com/item?id=' + this.props.item.id}>{this.props.item.title}</a>
    {
    this.props.item.url && <span className="newsItem-domain"><a href={'https://news.ycombinator.com/from?site=' + this.getDomain()}>({this.getDomain()})</a></span>
    }
    </div>
    );
    }
  2. 试着去掉 testData 的 url属性, 看看是不是一切正常.

  3. NewsItem 其余部分

    我们现在加上其余部分, 你已经看过了前两节, 这节应该是没有什么难度的, 我们快速带过.

    下载 grayarrow.gif, 在 NewsItem.js 中引入

    import ImageGrayArrow from './grayarrow.gif';

    修改 NewsItem.js

    getCommentLink() { // 评论链接
    var commentText = 'discuss';
    if(this.props.item.kids && this.props.item.kids.length) {
    commentText = this.props.item.kids.length + ' comment';
    } return (
    <a href={'https://news.ycombinator.com/item?id=' + this.props.item.id}>{commentText}</a>
    );
    } getSubtext() { // 分数, 作者, 时间, 评论数
    return (
    <div className="newsItem-subtext">
    {this.props.item.score} points by <a href={'https://news.ycombinator.com/user?id=' + this.props.item.by}>{this.props.item.by}</a> {Moment.utc(this.props.item.time * 1000).fromNow()} | {this.getCommentLink()}
    </div>
    );
    } getRank() { // 序号
    return (
    <div className="newsItem-rank">
    {this.props.rank}.
    </div>
    );
    } getVote() { // 投票
    return (
    <div className="newsItem-vote">
    <a href={'https://news.ycombinator.com/vote?for='+ this.props.item.id + '&dir=up&goto=news'}>
    <img src={ImageGrayArrow} width="10" />
    </a>
    </div>
    );
    } render() {
    return (
    <div className="newsItem">
    {this.getRank()}
    {this.getVote()}
    <div className="newsItem-itemText">
    {this.getTitle()}
    {this.getSubtext()}
    </div>
    </div>
    );
    }
    这里计算时间间距我们使用了 Moment, 如果你要使用你需要安装并引入它, 或者使用你喜欢的实现方法. NewItem.css .newsItem-rank {
    flex-basis: 25px;
    font-size: 10pt;
    text-align: right;
    } .newsItem-vote {
    flex-basis: 15px;
    text-align: center;
    } .newsItem-subtext {
    font-size: 7pt;
    } .newsItem-subtext > a {
    color: #828282;
    text-decoration: none;
    } .newsItem-subtext > a:hover {
    text-decoration: underline;
    }

NewsList

上一节中为了测试 NewsItem, 我们定义了一个测试数据 testDataNewsList 中也只有一个 NewsItem, 而真实的情况不会只有一条资讯, 而应该是一组资讯, 每一条对应有一个 NewsItem, 本节中我们来实现这个功能.

首先我们确定传入的数据是一个数组, 其中每一个元素都是一条资讯, 至于这个数据由哪里传入, 怎么生成我们先不关心, 但我们可以用 this.props.items 获取到. NewsList 对于其中的每一个元素都生成一个 NewsItem.

下面是修改完的 render

 render() {
return (
<div className="newsList">
<NewsHeader />
<div className="newsList-newsItem">
{
(this.props.items).map(function(item, index) {
return (
<NewsItem key={item.id} item={item} rank={index+1} />
);
})
}
</div>
</div>
);
}
新建样式 NewsList.css .newsList {
background: #f6f6ef;
margin-left: auto;
margin-right: auto;
width: 85%;
}

目前的代码是没法运行的, 我们还没有取得数据传入给 NewsList, 这将在下一节完善.

Hacker News API

本节中我们使用 Hacker News API 来获取数据, 具体请参考 API 文档.

app.js

function get(url) {
return Promise.resolve($.ajax(url));
} get('https://hacker-news.firebaseio.com/v0/topstories.json').then( function(stories) {
return Promise.all(stories.slice(0, 30).map(itemId => get('https://hacker-news.firebaseio.com/v0/item/' + itemId + '.json')));
}).then(function(items) {
render(<NewsList items={items} />, $('#content')[0]);
}).catch(function(err) {
console.log('error occur', err);
});

items 就是处理完后的数据, 一个由资讯数据组成的数组, 我们将它作为属性传入 NewsList.

至此, 你已经完成了 Hacker News Front Page, 就像开头所说的, 这篇教程不会使你精通, 但你应该对 React / Webpack / 模块化有了大概的了解.

用React & Webpack构建前端新闻网页的更多相关文章

  1. Webpack构建前端项目

    前言 公司据说要搞前后端分离,趁这两天项目完成的差不多,抓紧时间学习一下前端知识 现在流行前端项目工程化,那么第一个问题就是如何创建工程(项目),第一次玩webpack 通过 NPM 创建项目 # 创 ...

  2. 使用webpack+vue.js构建前端工程化

    参考文章:https://blog.csdn.net/qq_40208605/article/details/80661572 使用webpack+vue.js构建前端工程化本篇主要介绍三块知识点: ...

  3. react+webpack 引入字体图标

    在使用react+webpack 构建项目过程中免不了要用到字体图标,在引入过程中报错,不能识别字体图标文件中的@符,报错 Uncaught Error: Module parse failed: U ...

  4. 《React+Redux前端开发实战》笔记3:基于Webpack构建的Hello World案例(下)

    2.使用React编码 下面正式开始使用React来编写前端代码. (1)npm安装react和react-dom: npm install react react-dom -S (2)用下面代码替换 ...

  5. 《React+Redux前端开发实战》笔记2:基于Webpack构建的Hello World案例(上)

    这次搭建分为两部分:一部分是前期必要配置,一部分是开发React代码. [基于Webpack的React Hello World项目] 1.前期必要配置 (1)首先要确保读者的开发设备上已经安装过No ...

  6. 《React+Redux前端开发实战》笔记1:不涉及React项目构建的Hello World案例

    本小节实现一个不涉及项目构建的Hello World. [React的第一个Hello World网页] 源码地址:https://jsfiddle.net/allan91/2h1sf0ky/8/ & ...

  7. (24/24) webpack小案例--自己动手用webpack构建一个React的开发环境

    通过前面的学习,对webpack有了更深的认识,故此节我们就利用前面相关知识自己动手用webpack构建一个React的开发环境,就算是一个小案例吧. 注:此处使用的开发工具是Webstorm. 1. ...

  8. React项目构建(利用webpack打包)

    引言 最近React作为当前最为火热的前端框架.最近也相继而出来相关ES7的新语法. 当然,在使用React开发web项目的时候,不得不提到的就是与之配套的相应的打包技术,之前上文已经简单的提到Rea ...

  9. 现代前端库开发指南系列(二):使用 webpack 构建一个库

    前言 在前文中,我说过本系列文章的受众是在现代前端体系下能够熟练编写业务代码的同学,因此本文在介绍 webpack 配置时,仅提及构建一个库所特有的配置,其余配置请参考 webpack 官方文档. 输 ...

随机推荐

  1. pop(),del A[:], a[:] = b[:]/'str'/可迭代的

    s = ['a','ma','shi','ge'] s0 = s.pop(0) #---> 有返回值 print(s,s0) s1 = s.remove('shi') #---> 无返回值 ...

  2. LeetCode (45) Jump Game II

    题目 Given an array of non-negative integers, you are initially positioned at the first index of the a ...

  3. [luoguP1972] [SDOI2009]HH的项链(莫队 || 树状数组 || 主席树)

    传送门 莫队基础题,适合我这种初学者. 莫队是离线算法,通常不带修改,时间复杂度为 O(n√n) 我们要先保证通过 [ l , r ] 求得 [ l , r + 1 ] , [ l , r - 1 ] ...

  4. [1143] [CTSC2008]祭祀river(最大独立集 || 偏序集最大反链)

    传送门 网上说这是偏序集最大反链,然而我实在不理解. 所以我换了一个思路,先用floyd,根据点的连通性连边, 问题就转换成了找出最多的点,使任意两个点之间不连边,也就是最大独立集. ——代码 #in ...

  5. [bzoj2506]calc_分块处理

    calc bzoj-2506 题目大意:给一个长度为n的非负整数序列A1,A2,…,An.现有m个询问,每次询问给出l,r,p,k,问满足l<=i<=r且Ai mod p = k的值i的个 ...

  6. Java:PPT(X)转图片、PDF和SVG

    (一) 简介: 工作中,PowerPoint文档有时需要被转换为PDF/图像文件来存档.因为PDF或图片的页面布局是固定的,很难被修改且能被大多数设备打开,所以PDF或者图片比起PowerPoint格 ...

  7. hdfs是什么?

    参考:https://www.cnblogs.com/shijiaoyun/p/5778025.html hadoop分布式文件系统 1.hdfs是一个分布式文件系统,简单理解就是多台机器组成的一个文 ...

  8. NetworkManager的坑(如何让network manager不去管理网络端口)

    在CentOS上,有时你需要停止并禁用 NetworkManager.但这样做了之后,其实NetworkManager还在影响着你的端口. 比如你有端口配置如下: [root@compute02 ~] ...

  9. java监控工具jconsole

    jconsole可以监控本地和远程进程 jvisualvm

  10. 配置文件的备份和IOS 的备份

    分享到 QQ空间 新浪微博 百度搜藏 人人网 腾讯微博 开心网 腾讯朋友 百度空间 豆瓣网 搜狐微博 百度新首页 QQ收藏 和讯微博 我的淘宝 百度贴吧 更多... 百度分享 广场 登录 注册 关注此 ...