零、前端技术选型
1、多页应用
① 特点:
- 内容都是由服务端用模板生成,如JSP,Django等
- 每次页面跳转都要经过服务端,会给用户一定等待时间
- JavaScript更多只是做页面的事件、动画等
② 主要的类库:
- jQuery(快速操作DOM,兼容各类浏览器)
- YUI
③ 架构工具:
- 没有特定的前端工具,跟后端配合
- grunt / gulp
④ 模块化工具:
seajs
requirejs
⑤ 静态文件
使用gulp或grunt等工作手动编译到html中,自由度低,操作复杂,或者不处理,直接交给后端,让后端处理。
2、单页应用
① 特点:
- 所有内容都在前端生成
- JavaScript承担更多的业务逻辑,后端只提供API
- 页面路由跳转不需要经过后端,由前端生成路由
② 常用类库:
- Backbone.js
- Angular.js(typescript, .ts,http2)
- React.js(.jsx,单向数据流,方便数据管理)
- Vue.js(.vue, template,数据双向绑定)
③ 架构工具
- npm包管理器
- bower
- jspm(前端类库单独出来,使用http2,效率更高)
④ 模块化工具
- webpack
- rollup(按需加载,打包更快)
- browserify
⑤ 静态文件
可以直接在JavaScript代码中进行引用,并且交由模块化工具转化成线上可用的静态资源,可以定制转化过程适应不同的需求场景。
⑥ 其他考虑因素
- 浏览器兼容性
- 移动端还是PC端
- 面向市场用户(重交互,性能),还是面向企业使用(重安全,功能)
一、为什么要使用工程架构?
① 极大地解放了生产力,提高了开发效率
- 源代码预处理,将项目框架语言最终转化为浏览器能看懂的JavaScript
- 自动打包,自动更新页面显示
- 自动图片处理依赖,保证开发和正式环境的统一
② 围绕解决方案搭建环境
- 不同的前端框架需要不同的运行架构
- 语气可能出现的问题并规避
③ 保证项目质量
- 代码规范,使用Eslint配置
- 不同操作系统或编辑器下差异的排除,使用editorconfig配置
- git commit预处理,提交代码前,强制执行代码规范
二、webpack基本配置
1、什么是webpack?
Webpack is a module bundler for modern JavaScript applications.
webpack官方介绍,它是一个现代JavaScript应用诞生的模块打包器。
2、基础配置
// webpack.config.client.js
const path = require('path');
module.exports = {
entry: { // (1)
app: path.join(__dirname, '../client/app.js'),
},
output: { // (2)
filename: '[name].[hash:5].js', // (3)
path: path.join(__dirname, '../dist'), // (4)
publicPath: '/public' // (5)
}
}
- (1)
enrty
是指定了一个资源文件的路径 - (2)
output
指定了输出文件的目录 - (3)
filename
是输出文件名,可以设置带有hash值的名称,方便文件修改时文件名改变,避免浏览器缓存带来的不必要麻烦。 - (4)
path
用来存放打包后文件的输出目录 - (5)
publicPath
用来配置发布到线上的URL前缀,默认值为' ',即使用相对路径。
当需要构建出的资源文件上传到CDN服务商时候,为了加快页面的打开速度,需要如下配置:
filename: '[name]_[chunkhash:5].js',
publicPath: 'https://cdn.example.com/assets/'
此时发布到线上的HTML在引入JavaScript文件时,就需要如下配置项:
<script src="https://cdn.example.com/assets/a_12345.js"></script>
3、loader基础应用
// webpack.config.client.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: {
app: path.join(__dirname, '../client/app.js'),
},
output: {
filename: '[name].[hash:5].js',
path: path.join(__dirname, '../dist'),
publicPath: '',
},
module: {
rules: [
{
test: /.jsx$/,
loader: 'babel-loader', // (1)
},
{
test: /.js$/,
loader: 'babel-loader',
exclude: [ // (2)
path.join(__dirname, '../node_modules')
]
}
]
},
plugins: [
new HtmlWebpackPlugin() // (3)
]
}
- (1) 当页面需要引入一些非原生JavaScript的模块(module)时,需要在module中设置各种loader进行操作,方便浏览器识别,如对react的
.jsx
文件进行支持,需要引入babel-loader
- (2) 不需要对node_modules中的js进行编译,则需要用
exclude
进行排除,同时需要设置在项目根目录设置.babelrc
,如下,对es6和react进行编译和宽松的支持。
// .babelrc
{
"presets": [
["es2015", {"loose": true}],
"react"
]
}
- (3) webpack 通过
plugins
安装插件类实现一些功能,常用的如,HtmlWebpackPlugin
,它依据一个简单的模板,生成最终的html文件,这个文件中自动引用了打包后的JS文件。每次编译都在文件名中插入一个不同的带有哈希值的JS文件。
三、服务端渲染基础配置
1、为什么需要服务端渲染(SSR:Server-Side-Render)
- (1) SEO不友好,单页应用在浏览器端渲染HTML页面,搜索引擎不会去执行JS代码,不便于搜索引擎的录入
- (2) 用户体验不好,页面渲染每次要等待JS加载完成之后才出现,首次请求等待时间较长,会出现一个短暂的白屏。
2、React中怎么使用服务端渲染
react-dom
是React专门为web端开发的渲染工具,可以在客户端用react-dom的render方法渲染组件,而服务端,react-dom/server
提供我们将react组件渲染成HTML的方法。
// 入口文件app.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App.jsx';
ReactDOM.hydrate(<App />, document.getElementById('root')); // (1)
- (1) hydrate 描述的是 ReactDOM 复用 ReactDOMServer 服务端渲染的内容时尽可能保留结构,并补充事件绑定等 Client 特有内容的过程。
服务端渲染时使用Nodejs,Nodejs是没有document或者window对象的,需要创建一个特定的js文件(server-entry.js)去导出要在服务端渲染的内容,如下:
// server-entry.js
import React from 'react';
import App from 'App.jsx';
export default <App />;
因为这个server-entry.js
需要在Nodejs中执行,所以导出的jsx格式是需要webpack打包编译的,所以需要再建一个webpack配置文件,如下:
// webpack.config.server.js
const path = require('path');
module.exports = {
target: 'node', // (1)
entry: {
app: path.join(__dirname, '../client/server-entry.js'),
},
output: {
filename: 'server-entry.js', // (2)
path: path.join(__dirname, '../dist'),
publicPath: '/public',
libraryTarget: 'commonjs2', // (3)
},
module: {
rules: [
{
test: /.jsx$/,
loader: 'babel-loader',
},
{
test: /.js$/,
loader: 'babel-loader',
exclude: [
path.join(__dirname, '../node_modules')
]
}
]
},
// (4)
}
与webpack.config.client.js基本相同,个别地方做了一些特定的改动:
(1) target: 打包出来的内容执行环境设置,这里因为需要打包后放到服务端Nodejs中执行,所以选择
node
。还可以是web
,就是在浏览器端执行。(2) 因为服务端不会有缓存一说,所以不需要对压缩后的文件名进行hash处理,所以打包后的文件名不变。
(3) libraryTarget:设置打包出来的js模块方案,此处使用commonjs2规范,即最新的commonjs最新的模块加载方案,适用于Nodejs端。还可以设置
amd
,cmd
等。(4) 同时删除了HtmlWebpackPlugin,因为不需要服务端渲染时,不需要再生成一个Html文件了。
此时,需要对package.json中的scripts做一些修改:
"scripts": {
"build:client": "webpack --config build/webpack.config.client.js",
"build:server": "webpack --config build/webpack.config.server.js",
"clear": "rimraf dist", // (1)
"build": "npm run clear && npm run build:client && npm run build:server",// (2)
"start": "node server/server.js"
},
(1)
rimraf
是nodejs一个专门用来删除文件夹的包,这里是在每次打包压缩新的dist文件之前把之前的dist文件删除(2) build命令把build:client和build:server两个命令都去执行一遍
在client文件夹中新增一个t模板emplate.html文件,同时在webpack.config.client,js
中添加如下配置,将模板template.html合到导出后的index.html中。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="root"><app></app></div>
</body>
</html>
plugins: [
new HtmlWebpackPlugin({
template: path.join(__dirname, '../client/template.html')
})
]
当打包完成后,在服务端就可以开始渲染server-entry.js了:
使用Node.js的Express框架,来做服务端的渲染工作:
const express = require('express');
const ReactSSR = require('react-dom/server');
const serverEntry = require('../dist/server-entry.js').default; (1)
const fs = require('fs');
const path = require('path');
const app = express();
const template = fs.readFileSync(path.join(__dirname, '../dist/index.html'), 'utf8');
app.use('/public', express.static(path.join(__dirname, '../dist'))); // (2)
app.get('*', function(req, res) {
const appString = ReactSSR.renderToString(serverEntry); // (3)
res.send(template.replace('<app></app>', appString)); // (4)
});
app.listen(3333, function(){
console.log('server is running! Visit http://localhost:3333');
})
- (1)
const serverEntry = require('../dist/server-entry.js').default;
这里结尾之所以要加上.default,是因为用require引入的模块是整个内容,和import不同,当打印没有加.default的serverEntry时,可以发现如下:
{ __esModule: true,
default:
{ '$$typeof': Symbol(react.element),
type: [Function: App],
key: null,
ref: null,
props: {},
_owner: null,
_store: {} } }
default里才是需要的内容,所以需要找到default里。
(4) 用来区分静态内容还是服务端代码。
(3) 在浏览器端最终产生的是DOM元素,而在服务器端,最终产生的是字符串,因为返回给浏览器的是HTML字符串,所以服务器渲染不需要指定容器元素,只有一个返回字符串函数renderToString。
-
(4) 为了和浏览器端一致,把返回的字符串嵌入到id="root"的与元素里,即用appString代替index.js中id="root"里的'<app></app>'。
服务端渲染后的页面代码,如下图:
四、webpack-dev-server
Use webpack with a development server that provides live reloading. This should be used for development only.
根据官方的说明,可以知道,webpack-dev-server是一个用在开发环境中的一个服务,可以实时更新。
1、安装
安装webpack-dev-server和cross-env包
npm install webpack-dev-server --save-dev
npm i cross-env --save-dev
2、使用
首先,在package.json中的scripts中增加一个"dev:client",如下:
"dev:client": "cross-env NODE_ENV=development webpack-dev-server --config build/webpack.config.client.js",
因为webpack-dev-server用于开发环境,所以在webpack配置文件中需要加以区分是开发环境还是生产环境,在webpack.config.client,js
添加如下配置:
将之前的module.exports={}
的写法改成const config={};
然后给一个if条件判断if(diDev) {}
;最后再module.exports = config;
const isDev = process.env.NPDE_ENV === 'development'; // (1)
const config = {
entry: {
app: path.join(__dirname, '../client/app.js'),
},
output: {
filename: '[name].[hash:5].js',
path: path.join(__dirname, '../dist'),
publicPath: '/public',
},
module: {
rules: [
{
test: /.jsx$/,
loader: 'babel-loader',
},
{
test: /.js$/,
loader: 'babel-loader',
exclude: [
path.join(__dirname, '../node_modules')
]
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: path.join(__dirname, '../client/template.html')
})
]
}
// 只有在开发中才使用的
if (isDev) {
config.entry = {
app:[
'react-hot-loader/patch',
path.join(__dirname, '../client/app.js')
]
}
config.devServer = { // (2)
host: '0.0.0.0', // (3)
port: '8088',
contentBase: path.join(__dirname, '../dist'),// (4)
hot: true, // (5)
overlay: { // (6)
errors: true
},
publicPath: '/public/', // (7)
historyApiFallback: { // (8)
index: '/public/index.html'
},
}
}
module.exports = config;
- (1) 定义一个开发环境给变量isDev
- (2) 设置一个devServer
- (3) 如果想外部其他人也能访问,host就设为0.0.0.0,默认为localhost
- (4) 告诉服务器从哪里提供内容。只有在你想要提供静态文件时才需要。因为静态文件是放在output中的path中配置,所以这里和output中的path路径一致就行。
- (5) 启用 webpack 的模块热替换(hot-module-replacement)特性
- (6) 如果webpack在编译过程中出错,则在网页显示信息
- (7) 此路径下的打包文件可在浏览器中访问,确保 publicPath 总是以斜杠(/)开头和结尾。
假设服务器运行在 http://localhost:8080 并且 output.filename 被设置为 bundle.js。默认 publicPath 是 "/public/",所以你的包(bundle)可以通过 http://localhost:8080/public/bundle.js 访问。 - (8) 任意的 404 响应被替代为设置的index
注意:当启动webpack-dev-server时,最好是删除之前已经打包的文件,如dist文件夹,因为当启动devServer时候,默认在硬盘中检测打包项目目录,如果有,则直接访问这个dist文件下js文件的,但这个dist下的js文件和html引入的js文件名不一致,所以会造成404错误。
五、hot-module-replacement
可以在开发环境中,修改代码后,在页面中无刷新的看到代码变化后的效果,极大的提高了开发效率。
1、安装
安装react-hot-loader
插件,它提供了React的hot-module-replacement的功能的工具。
npm install react-hot-loader --save-dev
2、配置
配置.babelrc文件:
"plugins": [
"react-hot-loader/babel"
]
修改webpack.config.client.js文件,添加HotModuleReplacementPlugin:
const webpack = require('webpack'); // 引入webpack
if (isDev) {
config.entry = {
app:[
'react-hot-loader/patch',
path.join(__dirname, '../client/app.js')
]
}
// 在开发环境中加入一个HotModuleReplacementPlugin
config.plugins.push(new webpack.HotModuleReplacementPlugin())
}
修改app.js:
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App.jsx';
import { AppContainer } from 'react-hot-loader';
const render = Component => {
ReactDOM.hydrate(
<AppContainer>
<Component />
</AppContainer>,
document.getElementById('root')
)
}
render(App);
if (module.hot) {
module.hot.accept('./App.jsx', () => { // (1)
const NextApp = require('./App.jsx').default;
render(NextApp)
})
}
- (1) accept方法给定依赖模块的更新,并触发一个 回调函数 来对这些更新做出响应。
六、规范代码
常用的规范代码主要从代码规范和代码书写格式等方面进行规范,如eslint规范代码风格,editorconfig规范代码文本格式等。
1、为什么要规范代码
- (1) 规范代码有利于团队协作
- (2) 纯手工规范费时费力,而且不能保证准确性
- (3) 能配合编辑器(如vscode)自动提醒错误,提高开发效率
2、Eslint
安装:
npm install eslint --save-dev
npm install babel-eslint --save-dev
npm install eslint-config --save-dev
npm install eslint-config-airbnb --save-dev
npm install eslint-config-standard --save-dev
npm install eslint-loader --save-dev
npm install eslint-plugin-import --save-dev
npm install eslint-plugin-jsx-a11y --save-dev
npm install eslint-plugin-node --save-dev
npm install eslint-plugin-promise --save-dev
npm install eslint-plugin-react --save-dev
npm install eslint-plugin-standard --save-dev
配置:
在项目根目录创建一个.eslintrc文件,然后添加配置项:
{
"extends": "standard"
}
直接继承标准的eslint标准就行
另外,在client文件目录下再建一个.eslintrc文件,因为client文件中主要代码是用jsx语法来书写的,很多写法和nodejs端是不一样的,需要单独配置,如下:
{
"parser": "babel-eslint",
"env": {
"browser": true,
"es6": true,
"node": true
},
"parserOption": {
"ecmaVersion": 6,
"sourceType": "module" // (1)
},
"extends":"airbnb",
"rules": {
"semi": [0]
}
}
主要继承了美国Airbnb公司的React eslint规则。
- (1) 定义文件是 ECMAScript 模块
如果希望代码在每次编译之前使用这些eslint规则去检查一遍,可以在webpack.client.config.js中设置:
在module中增加一条rules:
{
enforce: 'pre', // (1)
test: /.(js|jsx)$/,
loader: 'exlint-loader',
exclude: [
path.join(__dirname, '../node_modules')
]
}
- (1) 在代码编译之前,执行eslint-loader,如果出现错误,停止编译,报出错误。
3、editorconfig
在根目录创建.editorconfig文件
注意:如果是在vscode编辑器中使用.editorconfig,需要安装一个插件 EditorConfig for VS Code,才能生效。
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
insert_final_newline = true
完整代码:请移步github