在小公司待了三年多,前端团队很小很小,没有前端大佬坐镇,彻底处于自我摸索的状态。两年前开始独立负责前端项目,热衷于本身手搭项目。对于那个时候的我来讲,一切都处于朦胧的状态,虽然有心想要把项目设计的更好,可是没有什么好的方向/思路(就好比刚开始写项目,调用后端接口都是分散在每一个模块中的,没有统一放在一个目录下去维护,若是后端接口变了,就须要全局搜索一个个的去修改接口...)。后来阅读了大量的书籍、文章、别人开源的项目以及惨痛的项目重构血泪史,渐渐地积累了一些项目经验,有了本身的积累(配置项目模板、写脚手架、搭建组件库...),渐渐的往前端工程化这个方向靠。javascript
写这篇文章的目的:给那些和我相同处境、喜欢本身手搭项目的小伙伴们一个参考,让初学者少走点弯路。若是有更好的建议还请告知,不胜感激。css
CSS reset 相对“暴力”,无论你有没有用,通通重置成同样的效果,且影响的范围很大,讲求跨浏览器的一致性。Normalize.css 不讲求样式一致,而讲求通用性和可维护性,是一种 CSS reset 的替代方案。它在默认的 HTML 元素样式上提供了跨浏览器的高度一致性。相比于传统的 CSS reset,Normalize.css 是一种现代的、为 HTML5 准备的优质替代方案。
html
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader','css-loader']
},
{
test: /\.less$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 2,
localsConvention: 'camelCase',
modules: {
localIdentName: '[name]__[local]--[hash:base64:5]'
},
}
},
'less-loader'
]
}
],
}
复制代码
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader','css-loader', 'postcss-loader']
},
{
test: /\.less$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 2,
localsConvention: 'camelCase',
modules: {
localIdentName: '[name]__[local]--[hash:base64:5]'
},
}
},
'postcss-loader',
'less-loader'
]
}
],
}
复制代码
postcss.config.js前端
// postcss-loader 会自动查找并调用这个文件
const autoprefixer = require('autoprefixer');
module.exports = {
plugins: [autoprefixer()],
};
复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge, chrome=1"/>
<meta name="renderer" content="webkit"/>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>首页</title>
<%htmlWebpackPlugin.options.dependencies.css.forEach(css=>{%>
<link rel="stylesheet" href="<%= css %>">
<%})%>
</head>
<body>
<div id="root"></div>
<%htmlWebpackPlugin.options.dependencies.js.forEach(js=>{%>
<script src="<%=js%>"></script>
<%})%>
</body>
</html>
复制代码
webpack.html.config.jsjava
const HtmlWebpackPlugin = require('html-webpack-plugin');
/** * 生产环境资源包路径配置 */
const libs = {
// iconfont: {
// js: ['./fonts/iconfont.js'],
// css: ['./fonts/iconfont.css']
// },
};
/** * 开发环境资源包路径配置 */
if (process.env.NODE_ENV === 'development') {
Object.assign(libs, {
// iconfont:{
// js: ['//at.alicdn.com/t/font_xxx.js'],
// css: ['//at.alicdn.com/t/font_xxx.css']
// },
dll: {
js: ['/public/dll/dllLibs.dll.js']
}
});
}
function createHtmlWebpackPluginConfig(chunkName, path, modules, chunks) {
const config = {
favicon: './src/entry/favicon.ico',
filename: `${chunkName}.html`,
template: `${path || './src/entry/index.html'}`,
inject: true,
chunks: ['vendors', chunkName].concat(chunks),
chunksSortMode: 'dependency',
minify: {
removeComments: true,
collapseWhitespace: false
},
dependencies: {
css: [],
js: []
},
};
modules && modules.forEach(m => {
if (m && m.css && m.css.length > 0) {
config.dependencies.css = config.dependencies.css.concat(m.css);
}
if (m && m.js && m.js.length > 0) {
config.dependencies.js = config.dependencies.js.concat(m.js);
}
});
return new HtmlWebpackPlugin(config);
}
module.exports = [
createHtmlWebpackPluginConfig('index', '', [libs.dll]),
];
复制代码
const path = require('path');
const DllPlugin = require('webpack/lib/DllPlugin');
module.exports = {
mode: 'development',
context: path.resolve(__dirname, "../"),
entry: {
dllLibs: ['react', 'react-dom', 'lodash', 'antd', 'react-redux', 'redux','history', 'react-router-dom', 'connected-react-router','axios','events','moment','react-beautiful-dnd']
},
output: {
path: path.resolve('public'),
// 输出的动态连接库的文件名称,[name] 表明当前动态连接库的名称
filename: 'dll/[name].dll.js',
// 默认是 var 这个全局变量,若是以这种方式导出的话,只能用脚本的方式进行全局访问
libraryTarget: 'var',
// 存放动态连接库的全局变量名称,例如对应 libs 来讲就是 _dll_libs
library: '_dll_[name]',
},
plugins: [
new DllPlugin({
// 动态连接库的全局变量名称,须要和 output.library 中保持一致
// 该字段的值也就是输出的 manifest.json 文件 中 name 字段的值
// 例如 libs.manifest.json 中就有 "name": "_dll_libs"
name: '_dll_[name]',
// 描述动态连接库的 manifest.json 文件输出时的文件名称
path: path.join('public', 'dll/[name].manifest.json'),
}),
]
};
复制代码
npm run type-check
package.jsonreact
"scripts": {
"type-check": "tsc --watch",
}
复制代码
tsconfig.jsonwebpack
{
"compilerOptions": {
// 不生成文件,只作类型检查
"noEmit": true,
}
}
复制代码
tsconfig.jsonios
{
"compilerOptions": {
// 在解析非绝对路径模块名的时候的基准路径
"baseUrl": "./",
"paths": {
/*路径映射的集合*/
"@public/*": ["public/*"],
"@src/*": ["src/*"],
"@assets/*": ["src/assets/*"],
"@styles/*": ["src/assets/styles/*"],
"@common/*": ["src/common/*"],
"@components/*": ["src/components/*"],
"@library/*": ["src/library/*"],
"@routes/*": ["src/routes/*"],
"@store/*": ["src/store/*"],
"@server/*": ["src/server/*"],
"@api/*": ["src/server/api/*"],
"@utils/*": ["src/utils/*"]
}
}
}
复制代码
webpack.base.config.jsgit
resolve: {
plugins: [
// 将 tsconfig.json 中的路径配置映射到 webpack 中
new TsconfigPathsPlugin({
configFile: './tsconfig.json'
})
],
// 由于使用了 TsconfigPathsPlugin 插件,因此这里就不须要再映射路径了
// alias: {
// "@src": path.resolve('src'),
// "@public": path.resolve('public'),
// "@assets": path.resolve('src/assets'),
// },
}
复制代码
export interface RouteConfigDeclaration {
/** * 当前路由路径 */
path: string;
/** * 当前路由名称 */
name?: string;
/** * 是否严格匹配路由 */
exact?: boolean;
/** * 是否须要路由鉴权 */
isProtected?: boolean;
/** * 是否须要路由重定向 */
isRedirect?: boolean;
/** * 是否须要动态加载路由 */
isDynamic?: boolean;
/** * 动态加载路由时的提示文案 */
loadingFallback?: string;
/** * 路由组件 */
component: any;
/** * 子路由 */
routes?: RouteConfigDeclaration[];
}
export const routesConfig: RouteConfigDeclaration[] = [
{
path: '/',
name: 'root-route',
component: App,
routes: [
{
path: '/home',
// exact: true,
isDynamic: true,
// loadingFallback: '不同的 loading 内容...',
// component: Home,
// component: React.lazy(
// () =>
// new Promise(resolve =>
// setTimeout(
// () =>
// resolve(
// import(/* webpackChunkName: "home"*/ '@src/views/home/Home'),
// ),
// 2000,
// ),
// ),
// ),
component: React.lazy(() =>
import(/* webpackChunkName: "home"*/ '@src/views/home/Home'),
),
routes: [
{
path: '/home/child-one',
isDynamic: true,
component: React.lazy(() =>
import(/* webpackChunkName: "child-one" */ '@src/views/home/ChildOne'),
),
},
{
path: '/home/child-two',
isRedirect: true,
isDynamic: true,
component: React.lazy(() =>
import(/* webpackChunkName: "child-two" */ '@src/views/home/ChildTwo'),
),
},
],
},
{
path: '/login',
isDynamic: true,
isRedirect: true,
component: React.lazy(() =>
import(
/* webpackChunkName: "login" */
'@src/views/login/Login'
),
),
},
{
path: '/register',
isDynamic: true,
component: React.lazy(() =>
import(/* webpackChunkName: "register"*/ '@src/views/register/Register'),
),
},
],
},
];
复制代码
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"src/**/*.{js,jsx,ts,tsx}": [
"eslint --fix",
"prettier --write"
]
},
复制代码
models/register/index.tsgithub
// 添加状态
const INCREMENT = 'INCREMENT';
import { RootDispatch, RootState } from '@src/store';
export interface RegisterStateDeclaration {
pageName?: string;
count: number;
}
const state: RegisterStateDeclaration = {
pageName: 'register',
count: 0,
};
export default {
name: 'register',
state,
reducers: {
[INCREMENT]: (state: RegisterStateDeclaration, payload): RegisterStateDeclaration => {
// 打印输出的是一个 proxy 代理实例对象
// console.log(state);
state.count += 1;
// 最终要返回整棵 state 树(当前 model 的 state 树——login)
return state;
},
},
// 两种写法:一种用常量做为 key ,一种直接定义方法
effects: (dispatch: RootDispatch) => ({
// async incrementAsync(payload, rootState: RootState) {
async incrementAsync() {
await new Promise(resolve =>
setTimeout(() => {
resolve();
}, 1000),
);
// 派发 login 里面的 action
// dispatch.login.INCREMENT();
this.INCREMENT();
},
}),
// effects: {
// async incrementAsync(payload, rootState: RootState) {
// await new Promise(resolve =>
// setTimeout(() => {
// resolve();
// }, 1000),
// );
// this.INCREMENT();
// },
// },
};
复制代码
/** * 检测变量类型 * @param type */
function isType(type) {
return function(value): boolean {
return Object.prototype.toString.call(value) === `[object ${type}]`;
};
}
export const variableTypeDetection = {
isNumber: isType('Number'),
isString: isType('String'),
isBoolean: isType('Boolean'),
isNull: isType('Null'),
isUndefined: isType('Undefined'),
isSymbol: isType('Symbol'),
isFunction: isType('Function'),
isObject: isType('Object'),
isArray: isType('Array'),
};
复制代码
使用 ESLint + Prettier 来统一前端代码风格
用 husky 和 lint-staged 构建超溜的代码检查工做流