1 React前端工程架构 - 基本配置
Tue, Sep 18, 2018
0x0 概述
上一篇文章我们聊过,工程架构的目标,主要是保证「开发效率」的。包括:
- 解放生产力
- 环境搭建
- 质量保障
本篇文章,将会就React的实际配置做下做下初步说明
内容包括:
- npm、webpack、babel编译打包基础配置
- SSR(服务端渲染)基础配置
- dev server实现代码实时编译
- hot module实现代码热更新
- 服务端渲染的实时更新
- eslint保证代码质量
0x1 编译打包基础配置
webpack
webpack是一个模块打包器,核心是他的loader
- 通过loader处理某种类型的资源(js、css)
- 会把资源处理成浏览器能运行的代码(es5),浏览器里能完整使用
- 如果木有对应类型的loader,可以自己写一个
- 如vue自己写了一个loader处理它定义的特殊语法
I 基础配置阶段
1.在项目根目录下,把项目配置成npm的项目 - 通过生成的package.json文件描述依赖的npm包
$ npm init |
相关选项 - 一般情况下直接enter用默认值就好
{ "name": "fe-execise", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "repository": { "type": "git", "url": "git@gitee.com:azen/FE-execise.git" }, "author": "Azen", "license": "ISC" } |
2.安装webpack、react
$ npm i webpack $ npm i react |
3.新建文件夹 & 初始文件
- build文件夹:放配置文件以及工程中需要用到的脚本文件
- webpack.config.js
- client文件夹:放前端app
- app.js:应用入口
- 里面引用了App.jsx,然后把它mount到html中
- App.jsx:声明整个页面上面的内容
- app.js:应用入口
webpack.config.js
const path = require('path'); module.exports = { // 入口 entry: { app: path.join(__dirname, '../client/app.js') }, output: { filename: '[name].[hash].js', path: path.join(__dirname, '../dist'), publicPath: "public/" } }; |
path包:把相对路径转成绝对路径,防止跨操作系统时路径出错 - entry - 告诉webpack打包入口文件是app
output:
- filename: 指定输出的文件名
- 简单的可以写死app.js这个名字
- 一般这样写
- []里面放变量:
- name表示entry中对应的名字,这里是app
- hash是打包完后,对内容做的hash
- 一旦树目录下任何一个文件变动,hash会变
- 会更新浏览器缓存
- []里面放变量:
- path:打包好的输出路径 - /dist文件夹下
- publicPath:指定静态资源文件引用时的路径
- 用途:用于区分url对应的请求目标,是静态资源还是api请求之类的
- 配置nginx的时候更方便区分(有public的就把静态资源丢过去,没的就丢给api服务器)
- 用途二:部署到cdn上的话,publicPath写cdn的域名即可
- 后续还会看到它的效果
- 用途:用于区分url对应的请求目标,是静态资源还是api请求之类的
package.json
{ "name": "fe-execise", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "build": "webpack --config build/webpack.config.js" }, "repository": { "type": "git", "url": "git@gitee.com:azen/FE-execise.git" }, "author": "Azen", "license": "ISC", "dependencies": { "react": "^16.5.1", "webpack": "^4.19.0" }, "devDependencies": { "webpack-cli": "^3.1.0" } } |
scrpits:
- 定义build命令
- --config:webpack指令的参数,用于指定config文件
简单测试
$ npm run build |
结果:
- 生成dist/app.xxxxhashxxxx.js文件,就是打包好的文件了
- 生成的js文件:
- 前面部分是webpack处理模块时加的东东,不用管它
- 底部会是我们自己写的相关逻辑
II 简单业务文件 & 入口文件实现
App.jsx
import React from 'react' export default class App extends React.Component { render() { return ( <div>初次见面,请多关照~(๑^ں^๑)</div> ) } } |
app.js
$ npm i react-dom -S |
- -S:save,生产环境依赖
- -D:save-dev,开发环境依赖
import ReactDOM from 'react-dom' import App from './App.jsx' import React from "react"; ReactDOM.render(<App />, document.body); |
配置:
- react-dom是用来把react的组件渲染到dom上用的
- 和它对应的,react-native是把组件渲染到手机端
- import App.jsx:
- 目前没配webpack忽略后缀名,所以需要写jsx后缀名
- import React:
- 这里一定要引React的原因:
- 每一个jsx的标签(<App />),本质都是把标签内容作为参数,传给react库中的create_element方法,进而生成真正标签
- 这里一定要引React的原因:
webpack.config.js
需要给webpack配置,让它能识别react相关语法 - jsx文件和一些我们自定义的js文件中会用到react语法
module.exports = { ... module: { rules: [ { test: /.(jsx|js)$/, loader: 'babel-loader', exclude: [ path.join(__dirname, '../node_modules') ] } ] }, }; |
配置:
- rules数组:里面配很多loader
- test:告诉它哪种类型的文件需要编译 - 以.jsx或.js作为结尾的文件
- loader:指定使用「babel-loader」作为loader
- exclude:需要排除的文件 - node_modules文件夹下的所有文件 - 这些都是三方库文件,本身已经被编译过了
$ npm i babel-loader@7 babel-core -D |
babel
是一个能编译各种js语法的工具,编译出来的是浏览器里默认能执行的es5语法
- babel能支持jsx、es7、ts等等
- 以前的编译器complier是react官方自己做的编译工具,现在直接用babel就好
bable-loader:是一个webpack插件,不包含babel的核心代码,需要再安装下「babel-core」
因为依赖版本问题,这里需要指定下babel-loader@7
babel默认只能编译es6的代码,不能编译jsx,需要在根目录写下配置文件`.babelrc`
{ "presets": [ ["es2015", {"loose": true}], "react" ] } |
配置:
- presets:配置支持的语法
- babel默认把所有语法拆分出去了,babel core没包含语法相关引擎
- es2015(就是es6),loose true是指松散型的语法
- react
安装babel语法引擎
npm i babel-preset-es2015 babel-preset-es2015-loose babel-preset-react -D |
简单测试
npm run build |
结果:
- 编译出来一个非常大的文件,里面包含了所有react源代码
浏览器展示
希望在浏览器中展示js效果,需要安装html-webpack-plugin
作用:
- 生成一个html入口页面
- 在webpack编译的时候,所有entry都注入到这个html入口页面中(在html中做引用)
- 路径会根据我们webpack配置的publicPath拼接而成的
安装:
$ npm i html-webpack-plugin -D |
修改配置文件,把插件配置给webpack
const HTMLPlugin = require('html-webpack-plugin'); module.exports = { ... plugins: [ new HTMLPlugin() ] }; |
效果:
- 在生成的入口html中注入了打包好的js文件引用 - 且拼接了publicPath配置中的前缀
- 因为没起任何服务,也没做路径映射,直接打开这个html是无法访问到我们的js文件的(因为真实文件路径中,dist目录下,js文件和入口html文件同级,真实目录中没有「public/」这一层)
- 为了展示效果,可以暂时注释掉publicPath配置
最终结果
我们看到,源代码里只有一句js引用,木有我们场景的<a><p>等标签,这样对SEO不利 - 蜘蛛认为我们的网站中木有内容
下一节解决这个问题
0x2 服务端渲染基础配置
0 概述
webapp开发模式下,都是在浏览器端使用js实时渲染出来内容的
问题:
- SEO不友好
- 根据url请求到的html是空白页面
- 蜘蛛会认为网站没东西
- 首次加载时间长,体验不好
解决思路:
服务端渲染:在服务端的nodejs环境下,提前做好渲染,得到要访问页面的html内容,返回给浏览器
可用工具:
- react-dom解决方案
- 客户端使用react-dom在浏览器中做页面渲染
- 服务端使用react-dom下面的server模块,在服务端做渲染
I 代码调整阶段
之前app.js文件中,我们直接把App直接mount到document.body
问题:服务端运行环境中木有document,这个东东只有浏览器端有
解法:定义server-entry.js文件作为服务端渲染入口文件
import App from './App.jsx' import React from "react" export default <App /> |
- 也需要打包和编译
- 因为jsx不能直接在nodejs环境下执行
配置:新建一个webpack配置文件,用来做服务端编译配置
build/webpack.config.server.js
const path = require('path'); module.exports = { target: 'node', entry: { app: path.join(__dirname, '../client/server-entry.js') }, output: { filename: 'server-entry.js', path: path.join(__dirname, '../dist'), publicPath: '', libraryTarget: 'commonjs2' }, module: { rules: [ { test: /.(jsx|js)$/, loader: 'babel-loader', exclude: [ path.join(__dirname, '../node_modules') ] } ] } }; |
说明:
- target:配置打包出来的内容在什么环境中执行的
- web(浏览器端)
- node(nodejs环境,服务端渲染)
- filenam:服务端没有缓存概念,不需要hash,写死就好
- libraryTarget:打包出来的js使用的模块方案(module)
- 指定为commonjs2(还有umd amd啥的...)
- 使用commonjs2的模块加载方案,适用于nodejs端
- plugin:这个不需要,不用生成一个html文件
构建
先安装一个删除文件夹的小工具
$ npm i rimraf -D |
定义构建方法:
package.json
{ ... "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "clear": "rimraf dist", "build:client": "webpack --config build/webpack.config.client.js", "build:server": "webpack --config build/webpack.config.server.js", "build": "npm run clear && npm run build:client && npm run build:server" }, ... } |
在build之前,先把dist包清空下,删掉历史打的js包,避免编译结果污染
构建结果
生成了三个文件
服务端打包出来的server-entry.js文件,可以用来放在node.js环境中做渲染,生成对应的html文件
简略看下打包出来的server-entry.js文件
module.exports=function(e){...xxxxxx...} |
II 配置渲染服务器,构建nodejs渲染环境
需要起一个nodejs网络服务,使用express作为网络服务框架
$ npm i express -S |
实现渲染服务器
创建server文件夹,放web服务相关代码
实现server.js文件,用来实现web服务相关逻辑
const express = require('express'); const ReactSSR = require('react-dom/server'); const serverEntry = require('../dist/server-entry.js').default; const app = express(); app.get('*', function (req, res) { const appString = ReactSSR.renderToString(serverEntry); res.send(appString) }); app.listen(2333, function () { console.log('🚀 服务起来了,正在监听2333端口(✌゚∀゚)☞') }); |
- 需要引用过来react-dom/server这个包,用来做html服务端渲染
- 任何请求过来,都用SSR渲染之前用webpack build好的待渲染server-entry.js文件,返还给请求者
- 由于/dist/server-entry.js使用commonjs2的方式编译,导出方法为modules.exports=xxx,故nodejs中require的时候,需要追加.default,否则会报错
npm中配置起这个服务的script
{ ... "scripts": { ... "web:start": "node server/server.js" } ... } |
渲染内容不完整
问题
虽然渲染出了基础的内容 ,但是这个不是我们想要的完整内容:
- 没有这个页面的js文件引用
- 不是完整的html文件,只有一行语句
思路
把服务端渲染出来的html标签内容,拼接到编译打包好的客户端index.html文件的指定位置,然后把拼接后的内容返回给浏览器
方案
- 在client文件夹下写个模板文件template.html
- 渲染的时候,把模板文件中的占位文本(如<>)替换成服务端渲染出来的东东
template.html
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Webpack App</title> </head> <body> <div id="root"> <app></app> </div> </body> </html> |
app.js
... ReactDOM.render(<App />, document.getElementById("root")); |
webpack.config.client.js
... plugins: [ new HTMLPlugin({ template: path.join(__dirname, '../client/template.html') }) ] |
server.js
... const fs = require('fs'); const path = require('path'); const template = fs.readFileSync(path.join(__dirname, '../dist/index.html'), 'utf-8'); ... app.use('/public', express.static(path.join(__dirname, '../dist'))); app.get('*', function (req, res) { const appString = ReactSSR.renderToString(serverEntry); res.send(template.replace('<app></app>', appString)) }); ... |
说明:
- 需要把index.html读进来,注意指定读取的文本格式为utf8
- 需要注意,这里要读的,是dist/index.html,是编译好的html入口文件,而非直接读client/template.html
- app.use句:
- 配置url中/public下的内容,为访问静态资源
- 挂载静态资源访问请求,到../dist目录下
- 目的是为了防止静态资源获取类请求(xxx.js的请求),也被app.get相关规则限制住
- 需要在webpack配置文件中指定publicPath为/public,与这里一致
- 把模板中的占位标签替换掉
webpack.config.xxx.js
... output: { ... publicPath: '/public' }, ... |
配置结果
III 问题
- 每次修改了client目录下的代码,都需要重新build一遍才能生效
- 每次修改了web server的代码,都需要重新web:run一遍才能成功
下一章,我们来实现「开发时代码的实时更新」