[React-章节3]如何为react导入css和img?
接着上一篇 《[备忘录] 记录一下学习react中遇到的知识点》,这次我要将css和img引入到我的站点,下面看看具体怎么做吧。
CSS
我们写网站,一般会压缩成一个或者多个大css文件放在<head>里,这样就可以控制所有html标签的样式了。
而react是组件化开发模式,通过对站点拆解成多个component。每个component高内聚,包含了对应的jsx控制dom,同时也可以引入相关的css文件,这样组件可以整体保存以便在各处复用。
webpack可以实现这样的目标,它将js文件和css文件都视为模块,js文件引入css文件就和引入一个js文件一样简单。webpack会帮我们把jsx和css都转化成js操作,统一生成到bundle.js中,动态的画出dom树,并将css样式填充到head中。
下面3个链接做了一些相关的介绍,前两个比较基础,最后一个比较深入:
css-loader分析 阮一峰作品,解释了css-loader的各种用法,增强理解
我继续在上一篇的代码基础上做试验,首先为TodoItem这个组件编写一个TodoItem.css文件:
1 2 3 |
.TodoItemViewDiv { background-color: grey; } |
我定义了一个class,为其设置了背景颜色,那么现在我在TodoItem.es6中引入这个css文件以便渲染:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
import React from "react"; import InputField from "./InputField"; import style from "./TodoItem.css"; export default class TodoItem extends React.Component { constructor(props, context) { super(props, context); this.state = { editable: false }; this.toggleEditMode = this.toggleEditMode.bind(this); } toggleEditMode() { this.setState({ editable: !this.state.editable }); } renderViewMode() { const { title, completed, onToggle, onDelete } = this.props; // 注意这是jsx语法,不能写class而应该写className return ( <div className={style.TodoItemViewDiv}> <input type="checkbox" checked={completed} onChange={() => onToggle && onToggle(!completed)} /> <span onDoubleClick={this.toggleEditMode}>{title}</span> <button onClick={() => onDelete && onDelete()}>x</button> </div> ); } renderEditMode() { const { title, onUpdate } = this.props; return ( <InputField autoFocus placeholder="編輯待辦事項" value={title} onBlur={this.toggleEditMode} // 失去焦点 onKeyDown={(e) => { // ESC if (e.keyCode === 27) { e.preventDefault(); this.toggleEditMode(); } }} // 提交 onSubmitEditing={(content) => { onUpdate && onUpdate(content); this.toggleEditMode(); }} /> ); } render() { return this.state.editable ? this.renderEditMode() : this.renderViewMode(); } } TodoItem.propTypes = { title: React.PropTypes.string.isRequired, completed: React.PropTypes.bool.isRequired, onUpdate: React.PropTypes.func, onToggle: React.PropTypes.func, onDelete: React.PropTypes.func }; |
可以看到,我为div指定了{style.TodoItemViewDiv}这个className,并且在文件的头部引入了这个css文件。现在打开浏览器,并不会看到效果,因为我们并没有告诉webpack该怎么处理这个引入的css文件。
现在安装2个插件,上面的2个博客有一些介绍,这里不赘述。
最后编辑webpack.config.js,配置如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
module.exports = { entry: ["./app.es6"], output: { filename: "bundle.js" }, module: { preLoaders: [ { test: /\.js$/, exclude: /node_modules/, loader: 'jshint-loader' } ], loaders: [ { test: [ /\.js$/, /\.es6$/], exclude: /node_modules/, loader: 'babel-loader', query: { cacheDirectory: true, presets: ['react', 'es2015'] } }, { test: /\.css$/, loader: "style!css?modules" } ] }, resolve: { extensions: ['', '.js', '.es6'] } } |
添加了一个loader: “style!css?modules”项,要从右往左阅读,css?modules意思是调用css-loader插件,modules指示它应按模块重写css名称避免跨模块冲突。其产出的结果继续传递给style-loader生成style标签,最终实现css的引入。
现在访问页面,可以看到3个TodoItem都变成了灰色背景。我看了一下html发现div加上了class=”TodoItemViewDiv”,并且在head里多了一个style标签,里面写着TodoItem.css中的内容,整体效果如下:
查看网页源码,可以看到编译后的效果:
1 2 3 |
<head><style type="text/css">._3uk3wkzuQXtkxYS-yq9wXY { background-color: grey; }</style></head> |
1 |
<li><div class="_3uk3wkzuQXtkxYS-yq9wXY"><input type="checkbox" value="on"><span>Item 1</span><button>x</button></div></li> |
可见,通过import方式导入css,通过className指向style中的css项,webpack替我们重命名了相关的class,从而避免了跨component的冲突,简直太棒了。
提取css
在此之前,webpack将所有的css都转化成js操作编译到了bundle.js中。如果我们写了很多component,那么意味着css总体积很大,那么放到bundle.js中就不太合适了,所以很简单的想法就是把css从bundle.js中提取出来,直接当做一个css文件链入进来。
这里要用到一个新的插件,安装方法:npm install extract-text-webpack-plugin –save-dev。它的作用是将css-loader(编译单个css文件),style-loader(将css放到style标签)的最终产出物全部提取出来,放到一个独立的.css文件中存储。
为了使用它,我们只需要改一下webpack.config.js文件,引入并配置它:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
var ExtractTextPlugin = require("extract-text-webpack-plugin"); module.exports = { entry: ["./app.es6"], output: { filename: "bundle.js" }, module: { preLoaders: [ { test: /\.js$/, exclude: /node_modules/, loader: 'jshint-loader' } ], loaders: [ { test: [ /\.js$/, /\.es6$/], exclude: /node_modules/, loader: 'babel-loader', query: { cacheDirectory: true, presets: ['react', 'es2015'] } }, { test: /\.css$/, //loader: "style-loader!css-loader?modules" loader: ExtractTextPlugin.extract( "style-loader", "css-loader?modules" ) } ] }, plugins: [ new ExtractTextPlugin("bundle.css") ], resolve: { extensions: ['', '.js', '.es6'] } } |
这里引入了extract-text-webpack-plugin插件,然后注释掉了原先的css相关的loader,将最终产物交给了ExtractTextPlugin从而生成磁盘上的css文件。同时,还需要注册这个插件给webpack,在plugins中可以看到对应配置,意思是将所有css提取到bundle.css文件中。
搞定这些之后,我们重启服务器发现原先的css样式不见了!原来这个插件只负责提取css但并不会修改bundle.js去创建对应的<link>标签。我们可以先自己在index.html的<head>里主动引入一下,看一下bundle.css是否生效:
1 2 3 4 5 6 7 8 |
<html> <head> <link href="bundle.css" rel="stylesheet" type="text/css"> </head> <body> <script src="bundle.js"></script> </body> </html> |
果不其然,样式生效了!
自动外链引入css
既然extract-text-webpack-plugin插件无法通过link自动引入,那么只能搬出救兵插件了:HtmlWebpackPlugin。
安装方法:npm install html-webpack-plugin@2 –save-dev
这个插件支持几个功能,主要是帮我们生成index.html并引入我们的之前提取的css和bundls.js,同时它也能为我们的css和js资源进行唯一命名,用于实现浏览器缓存淘汰。
我引入了这个插件,通过配置令其生成一个首页new_index.html,避免覆盖我原先手写的index.html。同时我指定了css和js文件的输出名称包含了所属chunk的[name](当前我还没学到chunk是什么,目前默认一个项目就一个chunk,也就是一套bundle.css和bundle.js)和文件哈希值[hash:8],可以认为是文件哈希的前8个字符。
编辑之后的webpack.config.js是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
var ExtractTextPlugin = require("extract-text-webpack-plugin"); var HtmlWebpackPlugin = require("html-webpack-plugin"); module.exports = { entry: ["./app.es6"], output: { // bundle.js也进行了编码 filename: "bundle-[name]-[hash:8].js", hash: true, // 开启hash编码功能 }, module: { preLoaders: [ { test: /\.js$/, exclude: /node_modules/, loader: 'jshint-loader' } ], loaders: [ { test: [ /\.js$/, /\.es6$/], exclude: /node_modules/, loader: 'babel-loader', query: { cacheDirectory: true, presets: ['react', 'es2015'] } }, { test: /\.css$/, //loader: "style-loader!css-loader?modules" loader: ExtractTextPlugin.extract( "style-loader", "css-loader?modules" ) } ] }, plugins: [ // 提取的css文件名进行了编码 new ExtractTextPlugin("bundle-[name]-[hash:8].css"), // 引入了html-webpack-plugin自动生成html new HtmlWebpackPlugin({ title: 'app', filename : 'new_index.html', }) ], resolve: { extensions: ['', '.js', '.es6'] } } |
重新运行webpack-dev-server,访问localhost:8080/new_index.html,可以正常访问。在浏览器中查看源代码,可以看到生成的new_index.html符合我们的预期,它自动引入了bundle.js和bundle.css,并且文件名都唯一编码了:
1 2 3 4 5 6 7 8 9 |
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>app</title> <link href="bundle-main-c8ffeeb8.css" rel="stylesheet"></head> <body> <script type="text/javascript" src="bundle-main-c8ffeeb8.js"></script></body> </html> |
html-webpack-plugin还支持将index.html进行模板化配置,可以按自己的意愿订制首页,例如增加一些head里的meta标签,这里不深入。
引入图片
首先,webpack里一切都是模块,比如之前看到的css和js,其实img也被视为了模块。
这里我想为TodoItem这个组件加一个背景图片,所以我需要修改TodoItem.css添加一个background-img样式。
为了让webpack能够正确的识别图片并将其视为模块,需要为webpack安装2个插件:npm install file-loader url-loader –save-dev,插件的具体用途可以参考:只看url-loader部分。它主要做了2个事情,一个是将图片视为模块,另外是对图片进行可选的内联处理。
然后,需要修改webpack.config.js,令插件url-loader识别并作用到图片文件,配置如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
loaders: [ { test: [ /\.js$/, /\.es6$/], exclude: /node_modules/, loader: 'babel-loader', query: { cacheDirectory: true, presets: ['react', 'es2015'] } }, { test: /\.css$/, //loader: "style-loader!css-loader?modules" loader: ExtractTextPlugin.extract( "style-loader", "css-loader?modules" ) }, { // 小于8KB的图片使用base64内联 test: /\.(png|jpg)$/, loader: 'url-loader?limit=8192' } ] |
现在,我放置了一个a.jpg图片在项目目录下,编辑TodoItem.css添加一个backgound-image样式引用这个图片:
1 2 3 4 |
.TodoItemViewDiv { background-color: grey; background-image: url("./a.jpg"); } |
访问localhost:8080/a_index.html,可以看到背景图出现了。在浏览器中打开css文件,可以看到图片被内联处理了:
1 2 3 4 |
._3uk3wkzuQXtkxYS-yq9wXY { background-color: grey; background-image: url(data:image/jpeg;base64,/9j/4AAQSkZJRgA...); } |
但是这样编写css直接引入./a.jpg,只体现了插件对图片的内联预处理,并没有体现出模块的感觉。下面,我为TodoHeader组件添加一个<img>标签,通过模块化方式引入a.jpg并赋值到<img>的src属性,最终在页面顶部可以看到一个图片。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
import React from "react"; import img from "./a.jpg"; // 图片像模块一样引入 export default class TodoHeader extends React.Component { render() { const { title, username, todoCount } = this.props; // 直接src={img}赋值 return ( <div> <img src={img}/> <h1>{title}</h1> <span>哈囉,{username}:你有 {todoCount} 項未完成待辦事項</span> </div> ); } } TodoHeader.propTypes = { title: React.PropTypes.string, username: React.PropTypes.string, todoCount: React.PropTypes.number }; TodoHeader.defaultProps = { title: '我的待辦清單', username: 'Guest', todoCount: 0 }; |
通过Import引入图片就和引入模块一样,在webpack里一切皆模块。最终我在jsx里带入了img,重新访问页面,可以看到渲染后的结果像这样,url-loader同样作用于此种情况,完成了data编码:
1 |
<div><img src="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQ..."><h1>我的待辦清單</h1><span><!-- react-text: 6 -->哈囉,<!-- /react-text --><!-- react-text: 7 -->Jason<!-- /react-text --><!-- react-text: 8 -->:你有 <!-- /react-text --><!-- react-text: 9 -->3<!-- /react-text --><!-- react-text: 10 --> 項未完成待辦事項<!-- /react-text --></span></div> |
学单页应用之前,先学会多页应用
基于html-webpack-plugin,我们就可以实现一个多页面的站点了,怎么做呢?
首先,我们之前的webpack.config.js只配置了一个entry入口js,叫做app.es6。它包含了整个站点的所有功能,最终会打包成bundle.js从而生成整个index页面。
在webpack的概念里,每个entry也称作一个chunk,可以理解为打包后的一个单元,之前我们都是整个站点1个单元,是这样配置的:
1 2 3 4 5 6 7 |
module.exports = { entry: ["./app.es6"], output: { // bundle.js也进行了编码 filename: "bundle-[name]-[hash:8].js", hash: true, // 开启hash编码功能 }, |
这样最终只会生成1个bundle.js,1个bundle.css,1个index.html。其中[name]就是chunk的名字,这里我们用数组的方式[“./app.es6”]配置的entry,所以默认名字叫做main。下面,我们改一下entry的定义,配置2个entry入口,但是它们都使用同一套代码app.es6,这次我们用字典的形式配置,从而可以指定chunk的名字叫做a和b:
1 2 3 4 5 6 7 |
module.exports = { // 字典的形式,配置了2个entry入口,就会对应2个所谓的chunk // 一个叫做a,一个叫做b,编译的话会生成2套css和js entry: { a: "./app.es6", b: "./app.es6" }, |
现在,我们可以认为webpack中存在了2个chunk,一个叫做a,一个叫做b,也就是2个输出单元。接下来,我们需要继续使用之前的html-webpack-plugin定义输出2个html,并且2个html分别基于a和b这2个单元进行打包。相关的基础知识可以在这里补充一下:只看资源的编译和输出部分即可。
这里,我配置生成a_index.html包含了chunk a(也就是entry a),b_index.html包含了chunk_b(也就是entry b),配置如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
plugins: [ // 提取的css文件名进行了编码 new ExtractTextPlugin("bundle-[name]-[hash:8].css"), // 引入了html-webpack-plugin自动生成html // 生成chunk=a的html new HtmlWebpackPlugin({ title: 'a', filename : 'a_index.html', chunks: ['a'] // 在entry里我定义过了a这个chunk }), // 生成chunk=b的html new HtmlWebpackPlugin({ title: 'b', filename : 'b_index.html', chunks: ['b'] // 在entry里我定义过了b这个chunk }), ], |
重启webpack-dev-server,访问localhost:8080/a_index.html可以看到:
1 2 3 4 5 6 7 8 9 |
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>a</title> <link href="bundle-a-d6fa2d54.css" rel="stylesheet"></head> <body> <script type="text/javascript" src="bundle-a-d6fa2d54.js"></script></body> </html> |
再访问localhost:8080/b_index.html可以看到:
1 2 3 4 5 6 7 8 9 |
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>b</title> <link href="bundle-b-d6fa2d54.css" rel="stylesheet"></head> <body> <script type="text/javascript" src="bundle-b-d6fa2d54.js"></script></body> </html> |
可见,a_index.html引入了bundle-a-d6fa2d54.css和bundle-a-d6fa2d54.js,而b_index.html引入了bundle-b-d6fa2d54.css和bundle-b-d6fa2d54.js,一个多页面应用就这样实现了!
查看完整项目代码
点击这里,下载我的完整试验代码。由于package.json里包含了完整的依赖包列表,所以你只要执行一下npm update即可完成环境搭建,接着运行webpack-dev-server,即可访问localhost:8080/a_index.html和b_index.html了。
通过本篇博客,掌握了css提取,img引入,多页面应用的编写。并且理解了webpack的模块化含义,熟悉了几个常用的必备插件,为后续单页应用的学习铺平道路。
在下一篇中,我将深入chunk,看看如何将项目的公共模块单独提取为独立chunk(js文件),更加合理的组织项目代码。
如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~

楼主真是细心啊。
谢谢!
123
456
1