阅读 81

从0到1构建服务级前端面向C端项目(一)

整体架构

前期规划

由于当前项目面对C端 所以需要考虑到以下几点问题

  1. 首屏渲染时间
  2. SEO优化
  3. 开发效率(代码可服用性,多人协作开发环境...)

由上述简单几点构建出下图所示 image.png 其中前端代码应用当前主流react框架 并配合react中服务端渲染从而以尽量最大化覆盖了上述1,2,3三点。 而对于开发效率问题 由于涉及到多人协作且很多场景需在测试环境进行真机测试问题,因此打算采用pm2+ngnix方式进行布控应用项目,从而达到多环境部署实现同时并行真机测试。pm2+ngnix简单配置将于后面章节中讲出,首先我们先来看前端应用项目代码。

注⚠️ 本篇大幅为代码注释所讲解

技术栈

首先我们来看当前所需核心三方库(eslint,postcss等优化基建库置于后续不断添加)

"devDependencies": {
    "babel-core": "^6.26.3", // babel转码核心
    "babel-loader": "^7.1.5", // webpack babel-loader
    "babel-preset-es2015": "^6.24.1",// 转es5
    "babel-preset-react": "^6.24.1",// 处理jsx
    "babel-preset-stage-0": "^6.24.1",// 可以使用各种最新语法 其中最🐂🍺在可以在jsx写if else了
    "clean-webpack-plugin": "^4.0.0-alpha.0", // 清除webpack打包生成的老旧文件
    "html-webpack-plugin": "^5.3.1", // 自动生成html文件
    "nodemon": "^2.0.7",// 监听js文件变化 自动重运行
    "uglifyjs-webpack-plugin": "^2.2.0",// 压缩代码
    "webpack": "^5.35.0",
    "webpack-cli": "^4.6.0",
    "webpack-dev-middleware": "^4.1.0"// 测试环境监听代码改动自动重启 配合express使用
  },
  "dependencies": {
    "axios": "^0.21.1",// 请求必备http库
    "express": "^4.17.1",// 轻量应用服务器
    "react": "^17.0.2",
    "react-dom": "^17.0.2"
  }
复制代码

看完所需库 我们一起再来看一下大致总体目录文件(其中dist下由于测试环境我们用到webpack-dev-middleware所以其文件会直接注入内存而不会打包显示出来)

image.png

其中server/index.js就是我们的整体文件入口 目前我们只给予测试环境相关代码 线上环境将在后续章节给出。其中index.js代码如下

注⚠️:babel-register如果想要生效的话,其代码必须为引入才可以不能直接在当前引入babel-register的这个文件中

// index.js
require('babel-register')({
  presets: ['es2015', 'react', "stage-0"]
}); // 引入后其下方所有引入代码均可被上面配置presets所处理

if(process.env.NODE_ENV === 'production'){
  console.log('暂未构建')
} else {
  require('./app');
}
复制代码

接着我们顺着看app里面都有哪些东西,app中 主要为开启指定3000端口,并进行webpack的配置文件打包,当我们执行 webpack(webpackConfig)这句话时会生成一个Compiler对象。Compiler 模块是 webpack 的主要引擎, 大多数面向用户的插件会首先在 Compiler 上注册。

import React from 'react';
import path from 'path';
import express from 'express';
import webpack from 'webpack';
import webpackConfig from '../webpack/webpack.config'
import webpackDevMiddleware from 'webpack-dev-middleware'

const app = express(); // 创建应用实例
const port = 3000; // 监听3000端口
const compiler = webpack(webpackConfig); // 获取compiler对象

app.use(webpackDevMiddleware(compiler, { // 使用dev中间件从而实现更改文件后自动刷新
  publicPath: webpackConfig.output.publicPath // 输出
}));

app.use(function (err, req, res, next) { // 专门处理错误输出
  console.warn('错误处理中间捕获Exception', err);
  res.send('内部错误');
});

app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`)
});

复制代码

可以看到最关键的前端代码一定是在webpackConfig中,接下来我们便去webpackConfig中去看一看。其中整体思想为 服务端代码直接由../src/server/index文件入口通过React中renderToString方法输出 并直接注入到根据模版生成的html中,但是由于某些element需要一些点击事件等等,所以当前思路为此类组件服务端先渲染一遍 客户端再渲染一遍从而绑定点击事件等

import HtmlWebpackPlugin from'html-webpack-plugin'; // 构建html插件
import { CleanWebpackPlugin } from 'clean-webpack-plugin'; // 清除上一次打包文件插件
import UglifyjsWebpackPlugin from 'uglifyjs-webpack-plugin'; // 压缩代码
import path from 'path';
import serverCode from '../src/server/index';// 服务端渲染代码

export default {
  mode:'development', // 开发环境
  entry:{ // 打包入口
    client:'./src/client/index.js',
  },
  output:{
    filename:'js/[name].[chunkhash:8].js', // 打包出口命名
    path:path.resolve(__dirname,'../dist'),
    publicPath: '/' // 服务器端路径 线上改为自己域名并记得在ngnix配置root
  },
  module:{
    rules:[
      {
        test:/\.js$/,
        exclude:/node_modules/,
        loader:'babel-loader',
        options: {
          presets: [
            'env' // 用bable-preset-env的规则进行转码
          ],
          cacheDirectory: true // 开启缓存,减少重复文件不断编译,增加编译效率
        }
      }
    ]
  },
  resolve:{
    extensions:['.js','.json','.scss'], // 没有后缀名时默认加
  },
  plugins:[
    new CleanWebpackPlugin(),
    // new GetJsFilesWebpackPlugin((jsArr)=>{ // 之前自己写的webpack插件 后来发现作用不大
    //   console.log('开始打包')
    // }),
    new HtmlWebpackPlugin({ // 配置html文件打包
      template:'./template/index.html', // 创建html文件模版 支持ejs语法
      title: '服务端渲染title', 
      body: serverCode, // 注入参数 名字可自定义 severcode为服务端渲染代码
      inject: false, // 不自动注入js 默认为true 因为想更加自定义js链位置所以默认关闭
      showErrors: true // 开启错误提示注入
    }),
    new UglifyjsWebpackPlugin(),// 压缩代码 测试环境其实也没必要开启
  ],
  optimization:{ // 优化配置
    splitChunks:{ // 分割代码
      chunks: 'all', // 分割包类型 async为异步加载的代码分割,all为所有都分割
      minSize: 20000,
      cacheGroups:{ // 分割缓存代码配置
        defaultVendors:{ 
          test: /[\\/]node_modules[\\/]/, // 第三方库都打一个包 从而实现缓存复用
          priority: -10,
          reuseExistingChunk: true, // 当chunks引用了已存在被抽离的chunks时不会新建一个而是复用
          name:'vendors' // 分割的名字,如果不加此属性会导致名字变得特别长
        },
        default: {
          minChunks: 2, // 多入口或异步加载 当有一个模块被共享次数大于2时 将给抽出
          priority: -20, //权重 当与上一条同时满足时 执行哪个
          reuseExistingChunk: true,
        }
      }
    }
  },
}
复制代码

我们来看一下服务端渲染代码,是不是非常简单 只要跟我们正常写react组件即可,最后调用renderToString方法,需要注意的是 所有需要js绑定的 我们都要放在客户端再执行一次。而且要注意在服务器端运行组件代码,不要带document对象等浏览器特有对象及其方法。

import React from 'react';
import { renderToString } from 'react-dom/server';
import App from '../common/app/index';

// 这里为了方便看 我们将App组件拿到这里看一下
// class App extends React.PureComponent{
//   handleClick=(e)=>{
//     alert(e.target.innerHTML);
//   }
//   render(){
//     return (
//     <h1 onClick={this.handleClick}>
//       Hello yuxiansen
//     </h1>);
//   }
// }
export default renderToString(<App/>);
复制代码

最后来看一下客户端代码 react-dom 提供了hydrate方法 采用hydrate时,hydrate的策略与render的策略不一样,其并不会对整个dom树做dom patch,其只会对text Content内容做patch。

import React from 'react';
import { hydrate } from 'react-dom';
import App from '../common/app/index';

hydrate(<App/>,document.getElementById("root"));
复制代码

好,由此详细的代码及注释相信你在本地也可以自己感受下 服务端渲染速度至极首屏开启的魅力,但在实际业务中一定要进行权衡,否则会对服务器产生较大的压力。后期将会进行style样式的抽离,eslint规范注入,生产环境打包,多环境部署的讲解。

文章分类
前端
文章标签