React 08 GRE 单词...

React 08 GRE 单词...

为了提高英语阅读水平, 找朋友要了一个GRE 官方核心词汇3300个的excel文件, 打算加上阅读来大量使用了. 之前其实也做好了相关准备, 比如手机上装好了mdict, 电脑上有golden dict, 准备开始用透析法来阅读原著, 然后有事没事就开始听东西. 不过由于单词都存放在Excel中,

为了提高英语阅读水平, 找朋友要了一个GRE 官方核心词汇3300个的excel文件, 打算加上阅读来大量使用了. 之前其实也做好了相关准备, 比如手机上装好了mdict, 电脑上有golden dict, 准备开始用透析法来阅读原著, 然后有事没事就开始听东西. 不过由于单词都存放在Excel中, 因此准备写一段程序, 把单词提取出来, 然后做一个页面, 用来提醒自己, 顺便就练习一下React和material-ui吧.
  1. 提取单词成为JSON文件
  2. 导入material-ui
  3. 准备基础数据
  4. Material-UI基础使用

提取单词成为JSON文件

这个用Python来操作就行了, 提取很方便. 准备提取成一个数组, 其中每个元素是一个对象, 属性如下: name是单词, meaning是单词的释义, 记住与否是该词是否记住, 是一个布尔值. 在用Python处理的过程中, 发现了一些错误, 以及无法识别成unicode的字符, 还发现一些单词缺少释义, 都补完了. 清洗数据的时候想到这文件不愧是英语高手常年维护的文件, 如果换成一本实体书的话, 怕不是都要翻烂了.清洗数据这个动词短语用来对付这种包浆的excel文件, 绝妙啊.
import random
import openpyxl

wb = openpyxl.open('words.xlsx')
names = wb.sheetnames[:-1]

def generateOnePair(word: str, explanation: str) -> str:
    return '{' +'name:"{}", meaning:"{}", remembered: false'.format(word, explanation)+'},'


count = 0
newfileIndex = 1
file = open('words.js', "wb+")
newWb = openpyxl.Workbook()
wordSheet = newWb.active
file.write('const words = ['.encode(encoding='UTF-8', errors='strict'))

for name in names:
    ws = wb[name]
    for i in range(1, ws.max_row + 1):
        if (ws.cell(row=i, column=1).value):
            count += 1
            word = ws.cell(row=i, column=1).value.replace("\n","")
            explanation = ws.cell(row=i, column=2).value.replace('\n',"")
            file.write(generateOnePair(word, explanation).encode(encoding='UTF-8', errors='strict'))
            wordSheet.cell(row=newfileIndex, column=1, value=word)
            wordSheet.cell(row=newfileIndex, column=2, value=explanation)
            newfileIndex += 1

file.write('];\nexport default words;'.encode(encoding='UTF-8', errors='strict'))

file.close()

newWb.save("result.xlsx")
> 代码很简单, 读取一下全部的单词和释义, 拼接成JS的对象, 然后再把结果也保存一份成为excel文件, 这样就毫无感情的统计好了生词以及生成了JSON文件, 接下来就是渲染.

导入material-ui

由于已经有了words.js, 就方便多了, 可以直接导入APP类中, 不过在此之前, 还是来使用一下material-ui的搭配吧. 用NPM安装material-ui的命令是:
npm install @material-ui/core
之后需要使用字体, 字体可以通过npm安装, 也可以通过链接导入, 一般通过链接导入, 由于google的字体被墙, 所以这里找了一个国内的字体代理如下:
<link rel="stylesheet" href="https://fonts.loli.net/css?family=Roboto:300,400,500,700&display=swap" />
其实就是用https://fonts.loli.net来替换google的官方字体网站. 然后是安装图标, 图标与上边的字体很类似:
npm install @material-ui/icons
图标可以通过链接来导入, 也使用上边提到的国内代理, 经过测试都没有问题:
<link rel="stylesheet" href="https://fonts.loli.net/icon?family=Material+Icons" />
最后是如果不使用React开发, 纯粹CDN导入, 可以使用如下两个地址:
https://unpkg.com/@material-ui/core@latest/umd/material-ui.development.js
https://unpkg.com/@material-ui/core@latest/umd/material-ui.production.min.js
最后就是略微的修改一下React项目默认的index.html的head标签中的meta元素:
<meta
        name="viewport"
        content="minimum-scale=1, initial-scale=1, width=device-width"
/>
下边就来使用material-ui美化一下吧. material-ui导入的实际上是React组件, 所以也是给其设置上不同的属性用来控制样式. 最后我使用的方案是, 字体通过链接导入, 其他的用到再说. 不过要注意, 字体如果不使用NPM, 还是需要手工设置一下的.

准备基础数据

这里创建一个新的React项目, 把app.js清空, 改成如下:
import React from 'react';
import './App.scss';
import words from "./data/words";

class App extends React.Component {

    constructor(props, context) {
        super(props, context);
        this.state = {
            words: words
        }
    }

    render() {
        return (
            <div className="App">
                <table className='word'>
                    <thead>
                    <tr>
                        <th>WORD</th>
                        <th>MEANING</th>
                    </tr>
                    </thead>
                    <tbody>

                    {this.state.words.map(
                        word => (
                            <tr key={word.name}>
                                <td><span className='name'>{word.name}</span></td>
                                <td><span className='meaning'>{word.meaning}</span></td>
                            </tr>
                        )
                    )

                    }


                    </tbody>
                </table>
            </div>
        );
    }
}

export default App;
由于在第一节里全自动生成了包含所有单词的对象, 所以将其直接导入成为app.state, 后边就是如何来使用的问题.

Material-UI基础使用

自己琢磨了一下, 其实这个玩意本质和其他UI框架没什么区别, 也是要上断点外加Grid系统. 我打算先一页显示一点图片试试, 最外层是一个container, 可以设置一个max的大小, 会自动居中, 这个比较常用. 有了material-ui之后, 项目自带的css也全部都可以删除了. 默认的断点如下: ** xs, ** 超小:0px ** sm, **小:600px ** md, **中等:960px ** lg, **大:1280px ** xl, **超大:1920px 于是就可以把app.js里的最外层容器直接替换掉:
render() {

    return (
        <Container maxWidth='md'>
            
        </Container>
    );
}
之后一个问题就是想把单词做成什么样子, 先尝试一下使用grid, xs屏幕上一行一个, sm及以上一行2个, 来试验一下:
<Container maxWidth='md'>
</Container>
最外层就是这个Container元素, 可以指定最大宽度, 然而这个组件通过prop.type规定了一定要有children, 所以还得往里边添加内容. 之后先来一行:
<Typography variant='h3' component='h3' gutterBottom align='center'>Words List Page {this.state.page}</Typography>
Typography是所有的文字排版组件, 根据要渲染成什么元素, 只要设置属性就可以了, 无论是标题还是普通文本都可以使用这个组件, 这就是复用的一大好处. 另外Link组件也需要套在Typography中间. 之后放一个按钮组, 用于控制翻页, 现在是每天150个词语, 感觉确实太可怕了.
<Box style={{'textAlign': 'center'}} m={2}>
<ButtonGroup color="primary" aria-label="outlined primary button group">
    {this.state.page === 1 ? null:<Button onClick={this.handleBack}>上一页</Button> }
    {this.state.page === 22 ? null:<Button onClick={this.handleForword}>下一页</Button> }
</ButtonGroup>
</Box>
之后要显示单词, 外层先套一个Grid, 属性为container:
<Grid container spacing={2} >
    {
        this.state.words.slice((this.state.page - 1) * 150, this.state.page * 150).map(
            word => (
                <WordCard name={word.name} key={word.name} mean={word.meaning}/>
            )
        )
    }
</Grid>
中间是一个组件, WordCard, 其内部实际上是一个Grid, 但是属性是item, 这个应该可以同时设置成item和container. 组件不复杂, 其实就是传单词和释义进去, 然后用一个Grid来展示:
import Grid from "@material-ui/core/Grid";
import React from "react";
import Typography from "@material-ui/core/Typography";

import './tap.scss'

class WordCard extends React.Component {

    constructor(props, context) {
        super(props, context);
        this.state = {
            meaning: null
        }
    }

    render() {
        return (
            <Grid item xs={12} sm={6} md={4} lg={3}>
                <Typography variant='body1'>{this.props.name}</Typography>
                <Typography className='tap' variant='body1'>{this.props.mean}</Typography>
            </Grid>
        )
    }
}

export default WordCard;
完整的App.js的类如下, state增加了计算总页数和控制当前页数的功能:
import React from 'react';
import words from "./data/words";
import Container from "@material-ui/core/Container";
import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography";
import WordCard from "./WordCard";
import ButtonGroup from "@material-ui/core/ButtonGroup";
import Button from "@material-ui/core/Button";
import Box from "@material-ui/core/Box";

class App extends React.Component {

    constructor(props, context) {
        super(props, context);
        const maxPage = Math.floor(words.length / 150) + 1;
        this.state = {
            words: words,
            number: words.length,
            page: 1,
            mapPage: maxPage
        }
    }

    handleBack = () => {
        if (this.state.page !== 1) {
            this.setState({page: this.state.page - 1})
        }
    }

    handleForword = () => {
        if (this.state.page !== 22) {
            this.setState({page: this.state.page + 1})
        }
    }

    render() {

        return (
            <Container maxWidth='md'>
                <Typography variant='h3' component='h3' gutterBottom align='center'>Words List Page {this.state.page}</Typography>
                <Box style={{'textAlign': 'center'}} m={2}>
                    <ButtonGroup color="primary" aria-label="outlined primary button group">
                        {this.state.page === 1 ? null:<Button onClick={this.handleBack}>上一页</Button> }
                        {this.state.page === 22 ? null:<Button onClick={this.handleForword}>下一页</Button> }
                    </ButtonGroup>
                </Box>
                <Grid container spacing={2} >
                    {
                        this.state.words.slice((this.state.page - 1) * 150, this.state.page * 150).map(
                            word => (
                                <WordCard name={word.name} key={word.name} mean={word.meaning}/>
                            )
                        )
                    }
                </Grid>
            </Container>
        );
    }
}
两个按钮的事件用于控制state中的page, 用来按照150个单词一页的方式进行展示. 应该说这个渲染还是挺重型的, 所有单词都保存在页面中, 每翻一次页就立刻渲染几乎全部的单词列表. 成品被我放在了https://conyli.cc/gre/words.html, 点击或者用鼠标悬浮到单词下的空白区域就可以显示出中文释义. 做这个东西的时候, 本来想使用翻译API, 后来发现没有免费的午餐, 开放的API全部都不支持跨域, 而想要使用基本都要交钱. 现在看来免费的数据真的也就剩下天气了. Material-UI基本搞清楚了, 就是自己没有网页设计这根筋, 还真的挺麻烦. 看来有时间要搭配后端一起来写了.
LICENSED UNDER CC BY-NC-SA 4.0
Comment